Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Open In Colab Open In Kaggle

Some data naturally travels with the operations that act on it: a weather station has a name, an elevation, and a growing list of readings, and the things you do with it — add a reading, compute its mean — belong to that station. A class bundles such state and behaviour into one object. This subchapter builds a WeatherStation, composes stations into a network, contrasts a lightweight dataclass record, and is careful about the opposite lesson too: when a plain function is the better tool. It then turns from writing code to trusting it: assert for internal invariants versus exceptions for bad input, validating physical preconditions, logging instead of printing, and testing with pytest — the concrete habits for checking code you did not write yourself, including code an AI assistant generated for you.

1.7.1 Classes and Instances

Every value used so far — a string, a list, a numpy array, a DataFrame — is an object: data bundled together with the operations that work on it, reached with dot notation (text.upper(), data.mean()). Until now those objects were always ones Python or a library already defined. A class is the blueprint that defines a new kind of object: what data it carries (its attributes) and what it can do (its methods). An instance is one specific object built from that blueprint — jungfraujoch and basel below are two different instances of the same WeatherStation class, each with its own name, elevation, and list of readings.

__init__ is the constructor: the method Python calls automatically each time a new instance is created, and its job is to set up that instance’s starting state. Inside any method, self refers to the particular instance the method was called on — it is how jungfraujoch.add_reading(0.4) updates only jungfraujoch’s readings, and basel.add_reading(18.0) only basel’s.

6.0
WeatherStation('Jungfraujoch', 3571 m, n=3)
WeatherStation('Basel', 316 m, n=3)
JFJ mean: -1.0 °C
JFJ at sea level: 23.2 °C

1.7.2 Instance versus Class Attributes

An instance attribute belongs to one object (each station’s own readings_celsius). A class attribute is shared by every instance (the single lapse_rate_celsius_per_km). Shared constants are a good use of class attributes; shared mutable state can lead to bugs.

-6.5 -6.5 -6.5
JFJ readings: [-2.3, -1.1, 0.4]
Basel readings: [18.0, 19.2, 17.5]

1.7.3 dataclasses: Records with Less Ceremony

When an object is mostly a bundle of fields, @dataclass generates __init__, __repr__, and __eq__ for you from type-annotated attributes.

Reading(timestamp='2024-06-01', temp_celsius=18.2)
18.2
True

1.7.4 Composition: Build Larger Objects from Smaller Ones

Composition is a has-a relationship: a StationNetwork has stations. The container delegates work to the objects it holds rather than re-implementing it.

StationNetwork('Switzerland', 2 stations)
coldest: Jungfraujoch

1.7.5 When Not to Use a Class

A stateless transformation needs no object. Wrapping a one-line conversion in a class adds boilerplate and hides a simple function behind a constructor.

291.34999999999997

1.7.6 Inheritance: Extending a Class

A subclass inherits a parent’s attributes and methods — an is-a relationship, as opposed to composition’s has-a. Calling super().__init__ reuses the parent’s constructor, so the subclass only has to set up what is new.

Rhone at Porte du Scex [12.4] [180.0]
mean temperature: 12.4

Prefer composition to deep inheritance hierarchies; inherit only for a genuine is-a relationship.

1.7.7 From Designing Objects to Trusting Code

The first half of this subchapter was about building your own objects. The second is a different skill: reading and stress-testing code you did not write yourself — including code generated by an AI assistant, which can run and look plausible while still being wrong on exactly the input you didn’t try. assert, exceptions, explicit validation, logging, and tests are the concrete tools for that: ways to make a piece of code state its own assumptions and get caught the moment those assumptions break, rather than trusting that code runs correctly just because it runs.

1.7.8 assert for Invariants

An assert documents and checks a condition that should always be true if the code is correct. It is a statement about the program’s own logic, not about external input — and it is removed when Python runs with the -O flag.

[0.25, 0.75]

1.7.9 Exceptions: raise, Custom Types, and try/except/else/finally

An exception signals a runtime problem. raise triggers one; a custom exception subclass names a specific failure; try/except/else/finally handles it — else runs when no exception occurred, finally always runs.

ok: 298.15 K
checked 25.0
rejected: -300.0 °C is below absolute zero
checked -300.0

1.7.10 Validating Physical Preconditions

External input — a file, a user value, a network response — must be validated with exceptions, not asserts, because it can be wrong even when the code is correct.

caught: discharge cannot be negative

1.7.11 Logging over print

print writes unconditionally to stdout. logging attaches a severity level to each message, so the same code can be verbose while debugging and quiet in production, and can route messages to files or services without edits.

INFO: loaded 45 readings
WARNING: 3 temperature values missing

1.7.12 Testing with pytest

pytest discovers functions named test_*, runs them, and reports failures with readable assertion output. A fixture supplies reusable setup; parametrisation runs one test over many cases. Here we write a small module and its test file to disk, then run the suite.

wrote thermo.py and test_thermo.py

When generated code lies: the shared class-level list

Asked for a station class, an assistant declares the readings list at the class level. That single list is then shared by every instance — the object-oriented version of the mutable-default-argument bug.

A readings: [10.0, 20.0]
B readings: [10.0, 20.0]
A readings: [10.0] | B readings: [20.0]

Summary

ConceptRule to remember
ClassesBundle state (attributes) with behaviour (methods); __init__ stores data on self.
AttributesInstance attributes are per-object, class attributes are shared — never put mutable state on the class body.
RecordsUse a dataclass for plain records, and composition to assemble larger objects from smaller ones.
InheritanceA subclass extends a parent; super().__init__ reuses its constructor.
When not toA stateless transformation should be a function, not a class.
assertFor internal invariants only — python -O strips it, so never validate external input with it.
Exceptionsraise for bad input, try/except/else/finally to handle it; fail early and loudly.
LoggingPrefer logging to print: messages gain a severity level and can be filtered or redirected.
Testingpytest — plain tests, fixtures for setup, parametrisation for many cases.

Resources