Model validated objects with data classes, properties, composition, explicit identity, and honest inheritance contracts.
A class defines a kind of object by combining durable state with behavior that protects or uses that state. Classes are valuable when instances have identity, invariants, lifecycle, or several related operations. They are not a requirement for every group of values: functions, tuples, dictionaries, and data classes often express simpler models more directly.
Learning goals
Decide when a class improves a model and when a function or mapping is simpler.
Create instances, initialize attributes, and write instance methods with self.
Protect invariants during construction and controlled updates.
Distinguish instance, class, and static methods.
Use properties for meaningful derived or validated attributes.
Apply data classes, composition, inheritance, and special methods deliberately.
Use a class when state and behavior belong together
A class is a useful boundary when values must remain valid together, behavior repeatedly operates on those values, or multiple objects need independent state. A namespace containing unrelated utility methods is not a meaningful object model. Start from the domain concept and its rules, not from a desire to use object-oriented syntax.
PYTHON · EDITABLE
classProgress:def__init__(self, completed:int, total:int)->None:if total <=0:raise ValueError("total must be positive")ifnot0<= completed <= total:raise ValueError("completed must be from 0 through total")
self.completed = completed
self.total = total
defpercentage(self)->float:return self.completed / self.total
progress = Progress(3,12)print(progress.percentage())
Classes and Data Models Tutorial | SovranCode
Progress is the class; progress is one instance. Each instance has its own completed and total attributes. Calling progress.percentage() binds progress to the method's self parameter automatically. self is a normal parameter name by convention and makes the instance being operated on explicit.
Establish invariants during construction
__init__ initializes an instance after Python creates it. Validate relationships before exposing a usable object so every successfully constructed instance satisfies its invariants. If validation raises, callers do not receive a partially initialized result.
PYTHON · EDITABLE
classLineItem:def__init__(self, name:str, unit_cents:int, quantity:int=1)->None:
clean_name = name.strip()ifnot clean_name:raise ValueError("name must not be blank")if unit_cents <0:raise ValueError("unit_cents must not be negative")if quantity <1:raise ValueError("quantity must be at least 1")
self.name = clean_name
self.unit_cents = unit_cents
self.quantity = quantity
deftotal_cents(self)->int:return self.unit_cents * self.quantity
Distinguish instance and class attributes
Attributes assigned through self belong to one instance. Attributes written in the class body are shared defaults or class-level data. Never use a mutable class attribute as if every instance owned a separate list or dictionary; all instances will observe the same object.
PYTHON · EDITABLE
classCourse:
platform ="SovranCode"# shared class attributedef__init__(self, title:str)->None:
self.title = title # per-instance attribute
self.lessons:list[str]=[]
python = Course("Python")
html = Course("HTML")
python.lessons.append("Functions")print(python.lessons)print(html.lessons)
Choose instance, class, and static methods
An instance method receives self and works with one object. A class method receives cls and is useful for alternate constructors or behavior tied to the class rather than one instance. A static method receives neither automatically; it is a namespaced function and should stay only when that ownership is genuinely useful.
PYTHON · EDITABLE
from datetime import date
classMembership:def__init__(self, learner:str, started_on: date)->None:
self.learner = learner
self.started_on = started_on
@classmethoddefstarting_today(cls, learner:str)->"Membership":return cls(learner, date.today())@staticmethoddefvalid_learner_name(value:str)->bool:returnbool(value.strip())
Alternate constructors should return cls rather than naming the current class so subclasses can inherit the behavior correctly. If a static method is broadly useful and does not strengthen the class's public model, a module-level function is often clearer.
Expose derived and validated attributes with properties
A property presents method-backed behavior through attribute syntax. Read-only properties are excellent for derived values. A setter can validate assignments, but methods with verbs are clearer when an update has significant effects or can fail for several business reasons.
Python relies on conventions rather than enforced private fields. A leading underscore marks an implementation detail. Double-leading underscores trigger name mangling, which helps avoid accidental subclass collisions but does not provide security. Protect secrets and authorization at system boundaries, not with attribute naming.
Use data classes for data-focused models
@dataclass generates common methods such as __init__, __repr__, and __eq__ from annotated fields. It reduces boilerplate without removing the need for modeling decisions. __post_init__ can validate generated initialization. frozen=True blocks ordinary reassignment, and slots=True avoids a per-instance attribute dictionary and prevents accidental new fields.
PYTHON · EDITABLE
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)classLineItem:
name:str
unit_cents:int
quantity:int=1def__post_init__(self)->None:ifnot self.name.strip():raise ValueError("name must not be blank")if self.unit_cents <0or self.quantity <1:raise ValueError("invalid price or quantity")@propertydeftotal_cents(self)->int:return self.unit_cents * self.quantity
Create safe data-class defaults
Mutable field defaults have the same sharing risk as function defaults. Data classes reject common mutable defaults and provide field(default_factory=...) so each instance receives a new value. Use factories for lists, dictionaries, sets, timestamps, UUIDs, or any value that must be created per instance.
PYTHON · EDITABLE
from dataclasses import dataclass, field
@dataclassclassLearningPlan:
learner:str
lessons:list[str]= field(default_factory=list)
first = LearningPlan("Amina")
second = LearningPlan("Salma")
first.lessons.append("Functions")print(first.lessons)print(second.lessons)
Integrate with Python through special methods
Special methods define how instances participate in Python protocols. __repr__ provides a developer-oriented representation, __str__ provides user-facing text, __len__ supports len, and comparison or hashing methods define value behavior. Implement only protocols that have an honest domain meaning.
Do not make surprising operator overloads. Adding two invoices, ordering users, or hashing mutable objects may be syntactically possible but semantically misleading. Protocol support should make the object behave like the concept readers already expect.
Prefer composition for has-a relationships
Composition builds an object from collaborating objects: an invoice has line items, and a course has lessons. Each component keeps its own rules and can be tested independently. This is usually more flexible than inheriting implementation merely to reuse code.
PYTHON · EDITABLE
classInvoice:def__init__(self, reference:str, items:list[LineItem]|None=None)->None:
self.reference = reference
self._items =list(items)if items isnotNoneelse[]@propertydeftotal_cents(self)->int:returnsum(item.total_cents for item in self._items)
Use inheritance for a genuine is-a contract
Inheritance says every subclass instance can be used wherever the base class is expected without breaking its promises. Use super to cooperate with base initialization. Keep hierarchies shallow and favor abstract behavior contracts over sharing fragile internal state.
PYTHON · EDITABLE
from abc import ABC, abstractmethod
classPriceRule(ABC):@abstractmethoddefapply(self, subtotal_cents:int)->int:raise NotImplementedError
classPercentageDiscount(PriceRule):def__init__(self, percent:int)->None:ifnot0<= percent <=100:raise ValueError("percent must be from 0 to 100")
self.percent = percent
defapply(self, subtotal_cents:int)->int:return subtotal_cents *(100- self.percent)//100
A subclass that strengthens input requirements, changes result meaning, or raises surprising new failures may violate substitutability. If variants only need interchangeable behavior, composition with a callable or protocol can be simpler than a class hierarchy.
Separate identity from value equality
is checks whether two references point to the same object. == asks whether objects represent equal values according to their type. Ordinary classes inherit identity-based equality unless __eq__ is defined; data classes generate field-based equality by default. Mutable value objects should not usually be hashable because changing equality-relevant fields would break dictionary and set membership.
PYTHON · EDITABLE
from dataclasses import dataclass
@dataclass(frozen=True)classCourseKey:
language:str
slug:str
first = CourseKey("python","functions")
second = CourseKey("python","functions")print(first == second)# equal valuesprint(first is second)# different instancesprint({first:"complete"}[second])
Keep domain objects separate from serialized data
JSON, database rows, and form payloads are boundary representations, not automatically valid domain objects. Parse and validate those structures, then construct the model. When exporting, create an explicit dictionary rather than exposing every internal attribute through __dict__.
PYTHON · EDITABLE
defline_item_from_dict(data:dict)-> LineItem:try:
name = data["name"]
unit_cents = data["unit_cents"]
quantity = data.get("quantity",1)except KeyError as error:raise ValueError(f"missing field: {error.args[0]}")from error
ifnotisinstance(name,str):raise TypeError("name must be text")return LineItem(name, unit_cents, quantity)
Common class-design mistakes
Creating a class that only groups unrelated static functions.
Validating in __init__ while allowing later assignments to break the same invariant.
Using mutable class attributes or mutable field defaults as per-instance state.
Adding getters and setters that provide no behavior beyond direct assignment.
Using inheritance only for implementation reuse.
Implementing surprising operator or equality behavior.
Treating frozen objects as deeply immutable.
Serializing internal attributes as a public data contract.
Build a validated invoice model
Run the notebook below, inspect the generated representations and totals, then add an invalid line item and read the validation failure. Add a new method that returns a formatted total without changing the integer-cents domain representation.
Identify each invariant and where it is protected.
Explain why LineItem is frozen but Invoice remains mutable.
Confirm that each Invoice receives its own item list.
Compare identity and equality for two equal line items.
Add a discount collaborator through composition rather than subclassing Invoice.
Lesson review
You can now decide when objects improve a model, construct valid instances, distinguish attribute ownership and method types, use properties and data classes, create safe defaults, implement honest protocols, compose collaborators, evaluate inheritance contracts, and preserve boundaries between serialized data and domain objects. Complete the checks and scored quiz before the final testing project.
RUNNABLE PYTHON NOTEBOOK
Build a validated invoice model
Create immutable line items, compose them into a mutable invoice, calculate exact totals, compare value equality, and trigger invariant failures.
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)classLineItem:
name:str
unit_cents:int
quantity:int=1def__post_init__(self)->None:ifnot self.name.strip():raise ValueError("name must not be blank")if self.unit_cents <0:raise ValueError("unit_cents must not be negative")if self.quantity <1:raise ValueError("quantity must be at least 1")@propertydeftotal_cents(self)->int:return self.unit_cents * self.quantity
@dataclass(slots=True)classInvoice:
reference:str
items:list[LineItem]= field(default_factory=list)defadd(self, item: LineItem)->None:
self.items.append(item)@propertydeftotal_cents(self)->int:returnsum(item.total_cents for item in self.items)deftotal_label(self)->str:returnf"MAD {self.total_cents /100:,.2f}"def__len__(self)->int:returnlen(self.items)
invoice = Invoice("INV-2026-001")
invoice.add(LineItem("Python notebook",1_995,2))
invoice.add(LineItem("Course guide",2_500))print(invoice)print("Items:",len(invoice))print("Total:", invoice.total_label())print("Equal values:", LineItem("Guide",500)== LineItem("Guide",500))assert invoice.total_cents ==6_490assert Invoice("EMPTY").items ==[]
OUTPUT
Press Run code to execute this Python program in your browser.
QUICK CHECK
Test what you learned
Which decorator generates common initialization and representation methods for data-focused classes?
QUICK CHECK
Test what you learned
Which parameter convention refers to the current instance inside an instance method?
QUICK CHECK
Test what you learned
Which function creates a fresh mutable default for each data-class instance?
KNOWLEDGE QUIZ
Check your class-design mental model
Choose an answer for every question, then check the explanations. The goal is not to memorize syntax; it is to predict what Python will do and why.