Fundamentals 17 min read

Six Alternatives to Classes in Python

This article compares six Python alternatives to traditional classes—plain classes, tuples, dictionaries, named tuples, attrs, dataclasses, and Pydantic—examining their syntax, validation capabilities, mutability, string representation, JSON (de)serialization, memory usage, and performance to help developers choose the most suitable data‑modeling approach.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Six Alternatives to Classes in Python

Developers constantly generate large amounts of data, and representing that data clearly is crucial. Using position data (longitude, latitude, optional address) as an example, the article explores six ways to model such data in Python.

Plain class : The standard way using a class with type annotations and explicit __init__ , getters, and setters. Example:

<code>from typing import Optional
class Position:
    MIN_LATITUDE = -90
    MAX_LATITUDE = 90
    MIN_LONGITUDE = -180
    MAX_LONGITUDE = 180
    def __init__(self, longitude: float, latitude: float, address: Optional[str] = None):
        self.longitude = longitude
        self.latitude = latitude
        self.address = address
    @property
    def latitude(self) -> float:
        return self._latitude
    @latitude.setter
    def latitude(self, latitude: float) -> None:
        if not (Position.MIN_LATITUDE <= latitude <= Position.MAX_LATITUDE):
            raise ValueError(f"latitude was {latitude}, but has to be in [-90, 90]")
        self._latitude = latitude
    @property
    def longitude(self) -> float:
        return self._longitude
    @longitude.setter
    def longitude(self, longitude: float) -> None:
        if not (Position.MIN_LONGITUDE <= longitude <= Position.MAX_LONGITUDE):
            raise ValueError(f"longitude was {longitude}, but has to be in [-180, 180]")
        self._longitude = longitude
</code>

Tuple : A lightweight immutable sequence. Example:

<code>from typing import Tuple, Optional
pos1 = (49.0127913, 8.4231381, "Parkstraße 17")
pos2 = (42.1238762, 9.1649964, None)

def get_distance(p1: Tuple[float, float, Optional[str]],
                 p2: Tuple[float, float, Optional[str]]) -> float:
    pass
</code>

Dictionary : The most common mutable mapping, storing attribute names as keys. Example:

<code>from typing import Any, Dict
pos1 = {"longitude": 49.0127913, "latitude": 8.4231381, "address": "Parkstraße 17"}
pos2 = {"longitude": 42.1238762, "latitude": 9.1649964, "address": None}

def get_distance(p1: Dict[str, Any], p2: Dict[str, Any]) -> float:
    pass
</code>

NamedTuple : An immutable tuple with named fields and a generated constructor. Example:

<code>from collections import namedtuple
attribute_names = ["longitude", "latitude", "address"]
Position = namedtuple("Position", attribute_names, defaults=(None,))
pos1 = Position(49.0127913, 8.4231381, "Parkstraße 17")
pos2 = Position(42.1238762, 9.1649964)

def get_distance(p1: Position, p2: Position) -> float:
    pass
</code>

attrs : A third‑party library that reduces boilerplate via the @attr.s decorator and attr.ib() field definitions. Example:

<code>from typing import Optional
import attr
@attr.s
class Position:
    longitude: float = attr.ib()
    latitude: float = attr.ib()
    address: Optional[str] = attr.ib(default=None)
    @longitude.validator
    def check_long(self, attribute, v):
        if not (-180 <= v <= 180):
            raise ValueError(f"Longitude was {v}, but must be in [-180, +180]")
    @latitude.validator
    def check_lat(self, attribute, v):
        if not (-90 <= v <= 90):
            raise ValueError(f"Latitude was {v}, but must be in [-90, +90]")
</code>

Dataclass : Built‑in since Python 3.7, providing similar functionality to attrs but using standard library decorators. Example:

<code>from typing import Optional
from dataclasses import dataclass
@dataclass
class Position:
    longitude: float
    latitude: float
    address: Optional[str] = None
    def __post_init__(self):
        if not (-180 <= self.longitude <= 180):
            raise ValueError(f"Longitude was {self.longitude}, but must be in [-180, +180]")
        if not (-90 <= self.latitude <= 90):
            raise ValueError(f"Latitude was {self.latitude}, but must be in [-90, +90]")
</code>

Pydantic : A third‑party library focused on data validation and settings management, offering both BaseModel and dataclass integration. Example:

<code>from typing import Optional
from pydantic import validator
from pydantic.dataclasses import dataclass
@dataclass(frozen=True)
class Position:
    longitude: float
    latitude: float
    address: Optional[str] = None
    @validator("longitude")
    def longitude_value_range(cls, v):
        if not (-180 <= v <= 180):
            raise ValueError(f"Longitude was {v}, but must be in [-180, +180]")
        return v
    @validator("latitude")
    def latitude_value_range(cls, v):
        if not (-90 <= v <= 90):
            raise ValueError(f"Latitude was {v}, but must be in [-90, +90]")
        return v
</code>

The article then discusses mutability and hashability, noting that immutable structures are preferable for hashing, while mutable ones require unsafe_hash or similar work‑arounds.

String representations are compared: plain classes give generic object refs, tuples show values without field names, dictionaries are verbose, while named tuples, attrs, dataclasses, and Pydantic provide readable, reconstructable output.

Data validation is highlighted: Pydantic aggregates all errors, whereas plain classes and attrs stop at the first failure.

JSON (de)serialization is demonstrated with Pydantic’s .json() and .parse_raw() methods, showing concise nested output, while dataclasses require manual conversion via asdict and attrs have similar limitations.

Memory consumption is measured, revealing that plain classes and tuples are smallest, attrs and dataclasses are moderate, and Pydantic models have the highest overhead (≈442 B for the dataclass version, ≈801 B for the BaseModel version).

Execution time considerations include object creation (with/without validation), JSON parsing, and dictionary parsing, with JSON parsing typically dominating.

Finally, the article provides guidance on when to choose each structure: use Dict when the schema is unknown, NamedTuple for fast, immutable grouping, Dataclass for mutable, type‑annotated objects, and Pydantic BaseModel for robust (de)serialization and validation.

performancePythonData ModelingClassesdataclassesalternativesPydantic
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.