W3docs

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.SimpleNamespace for dot-access
  • Using collections.namedtuple for 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 attribute

If 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)      # 31

Multiple 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 Bob

When 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.com

You 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

ToolMutableDot-accessMethodsType hintsBest for
dictYesNo (["key"])NoNoDynamic / unknown fields
SimpleNamespaceYesYesNoNoAd-hoc config, test fixtures
namedtupleNoYesNoPartialImmutable records, small data
classYesYesYesVia annotationsOOP with behaviour
@dataclassYes*YesYesYesStructured records with methods

*frozen=True makes a dataclass immutable.

Rule of thumb:

  • Use a dict when the structure is not known in advance.
  • Use SimpleNamespace when you want dot-access without a class definition.
  • Use namedtuple for simple, immutable records (coordinates, colours, rows).
  • Use a regular class when you need methods and full OOP.
  • Use @dataclass when you need a structured record with optional methods — it gives you the most for the least boilerplate.

Practice

Practice
In Python, what are the main reasons for grouping variables into classes?
In Python, what are the main reasons for grouping variables into classes?
Was this page helpful?