Chapter 12 of 24 | Python

User-defined types and data modeling

User-defined types model concepts in your program. A class groups state and behavior; a dataclass can generate common methods for record-like data. Choose a model that makes valid operations clear.

Define a class and protect its rules

class Counter:
    def __init__(self, start=0):
        if start < 0:
            raise ValueError("start must be non-negative")
        self._value = start

    def increment(self, amount=1):
        if amount < 0:
            raise ValueError("amount must be non-negative")
        self._value += amount

    @property
    def value(self):
        return self._value

__init__ initializes a newly created instance. self refers to that instance by convention. A leading underscore signals that an attribute is an implementation detail; Python does not enforce private access in the same way as some languages. A property provides attribute-like access while allowing validation or computed behavior.

Dataclasses for records

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float

    def distance_squared(self, other: "Point") -> float:
        return (self.x - other.x) ** 2 + (self.y - other.y) ** 2

@dataclass can generate initialization, representation, and equality methods from annotated fields. frozen=True prevents ordinary field reassignment and can support hashability when its fields are hashable. Frozen dataclasses are not deeply immutable if a field refers to a mutable object.

Composition and inheritance

Composition means one object contains or refers to another object and is often simpler than inheritance. Use inheritance when a subtype genuinely satisfies the behavioral contract of its base class. Python supports multiple inheritance and method resolution order; these features are powerful but can make initialization and method lookup hard to follow.

class EmailSender:
    def send(self, message):
        print("Email:", message)

class AlertService:
    def __init__(self, sender):
        self.sender = sender

    def alert(self, message):
        self.sender.send(message)

Passing a sender object makes the service easier to test with a fake implementation. This is dependency injection through composition, without requiring a shared base class.

Protocols and structural typing

Python is dynamically typed, but structural typing can describe behavior: an object is suitable if it provides the required operations. A Protocol from typing lets a static checker verify that shape without requiring explicit inheritance. This supports flexible APIs while retaining useful tooling feedback.

Beginner-to-master progression

Exercises

  1. Create a Book dataclass and a separate library class that manages books.
  2. Define a class invariant and enforce it in every operation that changes state.
  3. Replace inheritance in a small example with composition and compare the coupling.
  4. Describe a protocol for an object that can write text to a destination.

Data descriptors, properties, and slots

A property is a descriptor that computes an attribute or validates assignment. It can preserve a simple public interface while implementation evolves. Avoid turning every field into a property without a reason; direct dataclass fields are often clearer for plain records.

__slots__ can restrict instance attribute storage and may reduce per-instance memory in some designs, but it affects inheritance, weak references, and dynamic attributes. Measure before using it as a blanket optimization. It does not make an object immutable or guarantee a particular memory size.

Protocols, generics, and behavioral contracts

A protocol describes operations an object supports without requiring a shared base class. This is a form of structural typing. A generic class or function can preserve relationships between input and output types, while a protocol can constrain the operations required. Static checkers differ in supported features and strictness, so configure and test the checker as part of the project.

Static typing does not validate JSON, user input, or network payloads at runtime. Parse untrusted data into typed domain objects only after checking its structure and values. A type annotation should represent a validated invariant, not merely the shape you hope the data has.

Dataclass equality and hashing

Dataclasses can generate equality and ordering behavior based on fields. Decide whether all fields participate in equality and whether two instances with the same data represent the same value. Frozen dataclasses may be hashable when their fields are hashable; mutable dataclasses are generally not safe hash keys. Avoid using equality as an identity check for entities whose identity is independent from their current field values.

Mastery checkpoint

  1. Model a validated value object and specify its equality, ordering, and hashing semantics.
  2. Use a protocol to decouple a service from one concrete collaborator, then test with a fake.
  3. Measure memory before and after __slots__ on a realistic population and explain compatibility trade-offs.
  4. Validate raw external data before constructing a typed domain object.
0% read