Objects — study guide

The concept's fragments, read in order.

State and behavior in one bundle

data and behavior apart data no link functions bundle one object attributes (state) self.speed self.color methods (behavior) accelerate() set_speed() state and the behavior over it, kept together
An object bundles its state (attributes) and its behavior (methods) into one thing, instead of leaving data and the functions that act on it apart.

An object is a bundle: some data, and the functions that act on that data, kept together as one thing. The data is the object's state — a car's speed, a reading's value — and the functions bundled with it are called methods. Instead of loose variables sitting in one place and the functions that touch them sitting in another, an object holds both, so the behavior always travels with the state it works on.

A class is the plan for making such bundles, and an object is one thing built from that plan like an architect's blueprint: one drawing, many houses built from it, and each house owns its own address and paint while sharing the same plan. You write the class once to say what state every object of that kind carries and what methods it offers; then you build as many objects as you need, each with its own state but all sharing the same methods. The class is the type; the object is an instance of it, the same way 3 is an instance of int and [1, 2] is an instance of list.

The payoff is that related data and behavior stop drifting apart. When speed, heading, and the functions that change them live in one object, a caller cannot update the speed and forget the rule that goes with it, because the rule is a method on the same object. That bundling — state and the behavior over it in one place — is the whole reason classes exist, and the rest of this concept is the mechanics of writing one and building objects from it.

The blueprint and the thing built from it

A class statement defines a new type. Writing class Car: and an indented body does not build any car — it creates the type Car, a plan describing what every car will carry and do. Nothing has state yet; you have only said what a car is.

You build an actual object by calling the class name like a function: Car(). That call constructs a fresh instance — one concrete car — and hands it back for you to store in a variable. Call it again and you get a second, separate car. Each call produces a new object of the type, so Car() and Car() are two distinct things that happen to share the same plan.

The split that matters is between what is shared and what is per-object. The class is shared: every instance uses the same methods, defined once in the class body. The state is per-object: each instance carries its own attribute values, and changing one instance's speed leaves every other instance untouched. This is exactly the relationship a built-in type already has with its values — list is the shared type, and [1, 2] and [9] are separate instances with their own contents — and a class you write behaves the same way.

__init__ and self

the call Car('red', 0) construct __init__ self = the new object self.color = 'red' self.speed = 0 each argument becomes an attribute on self returns the object construction binds arguments to instance attributes via self
Calling Car of color red and speed 0 constructs a new object: __init__ runs with self bound to that object and stores each argument as an instance attribute on self, then the finished object is handed back.

When you call a class to build an object, Python creates the empty instance and then runs a special method named __init__ to set it up. Whatever you pass in the call — Car("red", 0) — is handed to __init__ as its arguments, and its job is to store those values on the new object as its starting state. Reaching the end of __init__ leaves you with a fully initialized object; it is the constructor step, run once per object at birth.

The first parameter of __init__, named self by convention, is the new object itself. Python passes it in automatically, so inside the body self is the very instance being built. Assigning self.color = "red" creates an attribute named color on that object and sets it; the object now carries color as part of its own state, separate from every other instance. Instance attributes come into being by being assigned on self — there is no separate declaration.

self is not special syntax; it is an ordinary parameter that happens to receive the instance. Every method takes it first, and it is always the object the method was called on, which is how a method knows whose state to read and change. Naming it self is a universal convention rather than a rule, but following it is what makes any Python class readable to the next person.

Functions that carry their data

A method is a function defined inside the class body. It looks like any function, except its first parameter is self, the instance it will act on. Because it lives on the class, every object of that type offers it, and because it takes self, it can read and change that particular object's attributes through self.speed, self.color, and the rest.

You call a method by naming the object, a dot, and the method: car.accelerate(). Python turns that into a call that passes car in as self for you, which is why the parentheses can look empty even though the method has a self parameter — the object before the dot is the argument that fills it. A method with more parameters takes its extra arguments in the parentheses as usual: car.set_speed(30) passes car as self and 30 as the next parameter.

The consequence is that a method only ever touches its own object's state like the start button on a washing machine: it acts on this machine's own drum and water, never the identical machine beside it, though every machine carries the same button. car.accelerate() changes car's speed and no other car's, because the self it received was car. This is the bundling that defines an object made concrete: the data lives on the object, the behavior is a method on the same object, and calling the method routes the behavior to exactly the state it belongs to. Reading and updating attributes through self is nearly all a method ever does.

__repr__ — how an object shows itself

Some method names are wrapped in double underscores, like __init__. These are dunder methods — short for double-underscore — and they are how a class hooks into the language's built-in operations. You rarely call them by name; Python calls them for you when the matching operation happens. __init__ runs when an object is constructed, and other dunder methods run when an object is printed, measured with len, or added with +.

The most useful one to add early is __repr__. It takes self and returns a string, and Python calls it whenever it needs to show the object — when you print it, when it appears inside a list, or when you inspect it in an interactive session. A good __repr__ returns something that reads like the code to rebuild the object, such as Point(x=2, y=5), so a printed object tells you what it holds at a glance.

Without a __repr__, an object still prints, but the default form shows only the class name and an internal identity code — enough to prove two objects differ, and nothing about their contents. Defining __repr__ replaces that with a description you choose. It changes no state and adds no behavior to the object's real job; it just makes the object legible to a human reading output or a debugger, which is worth its few lines the first time something goes wrong.

dataclass — skip the boilerplate

Many classes are mostly a bundle of named fields: a reading with a label and a value, a point with an x and a y. Written by hand, each one needs an __init__ that copies every argument onto self and a __repr__ that lists every field — lines that say nothing new and grow with each field you add. A dataclass writes them for you.

You mark the class with the @dataclass decorator, imported from the standard library's dataclasses module, and declare each field as a name with a type, like value: float. From those declarations the decorator generates the __init__ that takes the fields as arguments and stores them on self, and a __repr__ that renders as ClassName(field=value, ...). You write the fields once; the constructor and the printable form come out of them automatically, and the class stays a few readable lines no matter how many fields it carries.

A dataclass is the right first reach when a class is defined by the data it holds. When construction needs to check or transform its inputs, or the class is defined more by what it does than by the fields it stores, a hand-written __init__ earns its place again — the generated one only assigns. The two are not rivals: a dataclass removes the boilerplate when there is nothing but boilerplate, and you drop back to writing methods by hand the moment there is real behavior to express.

When a class earns its keep

A class is not free. It adds a type, a constructor, and a layer of ceremony, so it earns its keep only when it buys something back. The clearest signal is when state and the behavior that acts on it belong together: values that change over time, and rules about how they may change, that you would otherwise have to remember to apply by hand every time you touch the data. Bundling them into an object means the rules ride along with the values and cannot be left behind.

The sharper version of that signal is an invariant — something that must stay true across every operation. A temperature that must never read below absolute zero, a throttle that must stay between zero and full, an account balance that must match its transaction list. When operations share responsibility for keeping something consistent, a class gives them one place to live and one gate to pass through, so no caller can update half of a linked pair and leave the object in a state that should be impossible.

When there is no such pull, lighter tools win. Data with no behavior is often happiest as a plain dict, a named tuple, or a dataclass used purely as a record; a single computation with no state to carry is just a function. Reaching for a class before there is state-plus-behavior to bundle adds ceremony without the payoff. The judgment the concept leaves you with is that question: is there state, and behavior over it, that should travel together and stay consistent? When yes, a class earns its keep; when no, a simpler shape is the honest choice.