Fundamentals 13 min read

Avoiding the __init__ Anti‑Pattern in Python: Using dataclasses, classmethods, and NewType

The article explains why defining custom __init__ methods for simple data structures is a bad practice in Python and proposes a modern solution using dataclasses, classmethod factories, and typing.NewType to create clear, testable, and type‑safe class interfaces.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Avoiding the __init__ Anti‑Pattern in Python: Using dataclasses, classmethods, and NewType

Before Python 3.7 introduced dataclasses , the __init__ special method was essential for initializing data‑holding classes such as a 2DCoordinate with x and y attributes. Various work‑arounds (removing the class from the public API, exposing a factory function, using class attributes, or abstract base classes) suffered from serious drawbacks.

Because a simple __init__ that only assigns attributes has no obvious problems, it became the default choice, but it tightly couples object creation with side‑effects, leading to fragile code when requirements evolve.

Consider a more complex example: a FileReader class that wraps low‑level file I/O functions ( fileio.open , fileio.read , fileio.close ). The naïve implementation stores a file descriptor in a private attribute _fd and performs I/O in __init__ . While this works for simple use‑cases, it becomes problematic when additional behavior (e.g., custom opening logic, async support, testing without real I/O) is needed.

<code>class FileReader:
    def __init__(self, path: str) -> None:
        self._fd = fileio.open(path)
    def read(self, length: int) -> bytes:
        return fileio.read(self._fd, length)
    def close(self) -> None:
        fileio.close(self._fd)
</code>

To address these issues, three design steps are recommended:

Use dataclass to define attributes, letting Python generate a safe __init__ .

Replace the side‑effect‑ful __init__ with a classmethod factory that performs the necessary work and returns a fully‑initialized instance.

Employ precise typing (e.g., typing.NewType ) to distinguish raw integers like file descriptors from higher‑level abstractions.

Example using a dataclass and a classmethod factory:

<code>from typing import Self

@dataclass
class FileReader:
    _fd: int

    @classmethod
    def open(cls, path: str) -> Self:
        return cls(fileio.open(path))

    def read(self, length: int) -> bytes:
        return fileio.read(self._fd, length)

    def close(self) -> None:
        fileio.close(self._fd)
</code>

When async I/O is required, the factory can be defined as @classmethod async def open(...) , decoupling object construction from the limitations of __init__ .

To enforce validity of the file descriptor, typing.NewType can create a distinct type:

<code>from typing import NewType
FileDescriptor = NewType("FileDescriptor", int)
</code>

All low‑level functions are then annotated to accept FileDescriptor instead of a plain int , providing static‑type guarantees without runtime overhead.

Final refactored implementation combining dataclass, classmethod factory, and NewType :

<code>from typing import Self, NewType

FileDescriptor = NewType("FileDescriptor", int)

@dataclass
class FileReader:
    _fd: FileDescriptor

    @classmethod
    def open(cls, path: str) -> Self:
        return cls(_open(path))

    def read(self, length: int) -> bytes:
        return _read(self._fd, length)

    def close(self) -> None:
        _close(self._fd)
</code>

Key take‑aways:

Define data‑holding classes as dataclass (or attrs class) to get a reliable __init__ .

Provide explicit classmethod factories for object creation, keeping construction logic separate from the class definition.

Use precise typing (e.g., NewType ) to enforce validity of primitive values.

This approach yields easier testing, clearer APIs, and more maintainable code.

References:

[1] Python's built‑in file object abstraction: https://docs.python.org/3.13/library/io.html#io.FileIO

[2] Asynchronous I/O discussion: https://stackoverflow.com/questions/87892/what-is-the-status-of-posix-asynchronous-i-o-aio

[3] typing.NewType documentation: https://docs.python.org/3.13/library/typing.html#newtype

[4] attrs library: https://blog.glyph.im/2016/08/attrs.html

[5] __post_init__ in dataclasses: https://docs.python.org/3.13/library/dataclasses.html#dataclasses.dataclass.__post_init__

Pythonbest practicesobject-orienteddataclassestype hintsclassmethodnewtype
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.