Python Variables Group
Learn how to group related variables in Python using classes, dataclasses, named tuples, SimpleNamespace, and dicts — with runnable examples.
When a program needs to track several related pieces of data — a user's name, age, and email, for example — storing them in separate, unconnected variables becomes hard to manage. Python offers several tools for grouping variables under a single name so that they travel together and stay organised. This chapter explains the most common approaches, when to use each one, and the tradeoffs involved.
Topics covered:
- Why grouping variables matters
- Using a plain dictionary
- Using
types.SimpleNamespacefor dot-access - Using
collections.namedtuplefor lightweight immutable records - Using a class with
__init__ - Using
@dataclass(Python 3.7+) for the cleanest syntax - Choosing the right tool
Why Group Variables?
Suppose you are writing a script that processes user accounts. Without grouping you might write:
user_name = "Alice"
user_age = 30
user_email = "[email protected]"This works for one user, but breaks down the moment you need two users or pass data into a function:
def greet(name, age, email):
print(f"Hello {name}, age {age} ({email})")
greet(user_name, user_age, user_email)Three separate arguments must stay in sync everywhere. Grouping solves this by bundling the data:
user = {"name": "Alice", "age": 30, "email": "[email protected]"}
def greet(user):
print(f"Hello {user['name']}, age {user['age']} ({user['email']})")
greet(user)Now the function signature has one parameter instead of three, and adding a new field only touches the dictionary.
Using a Dictionary
A Python dictionary is the simplest way to group named variables. Keys are strings; values can be any type.
point = {"x": 10, "y": 20, "label": "origin"}
print(point["x"]) # 10
print(point["label"]) # origin
# Update a field
point["x"] = 15
print(point)
# {'x': 15, 'y': 20, 'label': 'origin'}When to use it: Quick one-off grouping, JSON data, situations where the set of fields is not fixed in advance.
Drawbacks: You access fields with string keys (point["x"]), which is more verbose than dot-notation and gives no IDE autocomplete.
Using types.SimpleNamespace
SimpleNamespace is a thin wrapper that gives you dot-access on an ad-hoc namespace without writing a class.
from types import SimpleNamespace
point = SimpleNamespace(x=10, y=20, label="origin")
print(point.x) # 10
print(point.label) # origin
# Update a field
point.x = 15
print(point)
# namespace(x=15, y=20, label='origin')SimpleNamespace objects are mutable — you can add, change, or delete attributes at any time:
from types import SimpleNamespace
config = SimpleNamespace(debug=False, timeout=30)
config.debug = True # update
config.retries = 3 # add new attribute
del config.timeout # remove
print(vars(config))
# {'debug': True, 'retries': 3}When to use it: Replacing a dictionary when you want dot-access but do not need methods or type checking. Good for test fixtures and simple configuration objects.
Using collections.namedtuple
A namedtuple is an immutable, lightweight record. It behaves like a regular tuple but lets you access fields by name as well as by index.
from collections import namedtuple
# Define the type once
Point = namedtuple("Point", ["x", "y"])
# Create an instance
p = Point(x=10, y=20)
print(p.x) # 10
print(p.y) # 20
print(p[0]) # 10 — index access still works
print(p) # Point(x=10, y=20)Because namedtuple instances are immutable, you cannot change a field after creation:
from collections import namedtuple
Color = namedtuple("Color", ["red", "green", "blue"])
white = Color(255, 255, 255)
# white.red = 0 # AttributeError: can't set attributeIf you need a modified copy, use the _replace() method — it returns a new instance:
from collections import namedtuple
Color = namedtuple("Color", ["red", "green", "blue"])
white = Color(255, 255, 255)
grey = white._replace(red=128, green=128, blue=128)
print(grey)
# Color(red=128, green=128, blue=128)When to use it: Immutable records where field names matter — coordinates, RGB colours, database rows. Smaller memory footprint than a full class.
Using a Class
For grouped variables that also need behaviour (methods), define a class with an __init__ method:
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
def greet(self):
return f"Hello, I am {self.name} and I am {self.age} years old."
alice = User("Alice", 30, "[email protected]")
print(alice.name) # Alice
print(alice.greet()) # Hello, I am Alice and I am 30 years old.
# Update a field
alice.age = 31
print(alice.age) # 31Multiple instances stay independent — each holds its own copy of name, age, and email:
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
alice = User("Alice", 30, "[email protected]")
bob = User("Bob", 25, "[email protected]")
print(alice.name, bob.name) # Alice BobWhen to use it: Whenever the grouped data also needs methods, validation logic, or inheritance. Classes are the foundation of object-oriented Python — see Python Classes and Objects for a full explanation.
Using @dataclass (Python 3.7+)
The @dataclass decorator auto-generates __init__, __repr__, and __eq__ from annotated class fields, removing most of the boilerplate:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
label: str = "unnamed"
p = Point(x=3.0, y=4.0)
print(p) # Point(x=3.0, y=4.0, label='unnamed')
print(p.label) # unnamed
p.label = "A"
print(p) # Point(x=3.0, y=4.0, label='A')Fields with a default value must come after fields without one (same rule as regular function arguments).
Immutable dataclass with frozen=True
Pass frozen=True to prevent any field from being changed after creation — similar in behaviour to a namedtuple but with full class capabilities:
from dataclasses import dataclass
@dataclass(frozen=True)
class RGB:
red: int
green: int
blue: int
white = RGB(255, 255, 255)
print(white)
# RGB(red=255, green=255, blue=255)
# white.red = 0 # FrozenInstanceError: cannot assign to field 'red'Grouping multiple records in a list
Dataclasses work naturally with lists when you need a collection of records:
from dataclasses import dataclass
from typing import List
@dataclass
class Product:
name: str
price: float
in_stock: bool = True
inventory: List[Product] = [
Product("Widget", 9.99),
Product("Gadget", 24.99),
Product("Doohickey", 4.50, in_stock=False),
]
for item in inventory:
status = "available" if item.in_stock else "out of stock"
print(f"{item.name}: ${item.price:.2f} ({status})")Output:
Widget: $9.99 (available)
Gadget: $24.99 (available)
Doohickey: $4.50 (out of stock)For the full feature set of dataclasses, including field(), __post_init__, and inheritance, see Python Dataclasses.
Grouping Variables with Class Attributes
Sometimes you want shared constants attached to a group rather than per-instance data. Class attributes (defined directly on the class body, outside __init__) are shared across all instances:
class AppConfig:
MAX_RETRIES = 3
TIMEOUT = 30
BASE_URL = "https://api.example.com"
print(AppConfig.MAX_RETRIES) # 3
print(AppConfig.BASE_URL) # https://api.example.comYou do not need to instantiate AppConfig to read its attributes — treat the class itself as a namespace for related constants. This is a lightweight pattern for configuration groups. For a fuller discussion of class attributes versus instance attributes, see Python Classes and Objects.
Choosing the Right Tool
| Tool | Mutable | Dot-access | Methods | Type hints | Best for |
|---|---|---|---|---|---|
dict | Yes | No (["key"]) | No | No | Dynamic / unknown fields |
SimpleNamespace | Yes | Yes | No | No | Ad-hoc config, test fixtures |
namedtuple | No | Yes | No | Partial | Immutable records, small data |
class | Yes | Yes | Yes | Via annotations | OOP with behaviour |
@dataclass | Yes* | Yes | Yes | Yes | Structured records with methods |
*frozen=True makes a dataclass immutable.
Rule of thumb:
- Use a
dictwhen the structure is not known in advance. - Use
SimpleNamespacewhen you want dot-access without a class definition. - Use
namedtuplefor simple, immutable records (coordinates, colours, rows). - Use a regular
classwhen you need methods and full OOP. - Use
@dataclasswhen you need a structured record with optional methods — it gives you the most for the least boilerplate.
Related Topics
- Python Variables — how variables work and naming rules
- Variable Names — naming conventions and best practices
- Global Variables — module-level variables and the
globalkeyword - Python Classes and Objects — full OOP explanation
- Python Dataclasses — deep dive into
@dataclass - Assign Multiple Values — unpacking and multiple assignment