Classes and Objects

Bundling data with the functions that work on it: what a class defines, what an instance holds, and what self actually is.

classes.py

classes.py Python 3
Output

                    

class_vs_instance.py

class_vs_instance.py Python 3
Output

                    

Worth knowing

The class is the template; each ClassName(...) call builds one instance.
__init__ runs on creation and sets up the instance's own data.
self is the instance. Python passes it in; you write it as the first parameter.
Attributes on self are per object. Attributes on the class are shared by all of them.

Classes and Objects: A Practical Guide

A class bundles data with the functions that operate on it. The class is a template; each instance built from it carries its own copy of the data and shares the behaviour.

Template and instance

class Dog:
    def __init__(self, name, age):
        self.name = name

Dog describes what a dog is. Dog("rex", 3) builds one. Build two and they are independent: changing a.age leaves b.age alone, because each instance has its own attributes.

What __init__ does

__init__ runs immediately after the instance is created, and its job is to set up that instance's data. It is not a constructor in the C++ sense — the object already exists by the time it runs — but in practice it is where you put everything the object needs to start life.

The double underscores mark it as a name Python itself calls. You almost never call __init__ directly.

self is not magic

self is the instance, handed to the method as its first argument. Python does this for you: a.speak() is Dog.speak(a), and the page prints both to show they are the same call.

The name self is convention rather than syntax — the first parameter could be called anything — but every Python programmer expects self, and using something else will read as a mistake.

Every method that touches instance data needs it, and every attribute belonging to the instance is reached through it. Forgetting self. inside a method is the most common early error: you get a local variable that vanishes when the method returns.

Class attributes are shared

class Counter:
    total = 0      # one, for everybody
    def __init__(self):
        self.count = 0  # one per instance

self.count belongs to the object. Counter.total belongs to the class and every instance sees the same one. This is occasionally what you want — a registry, a shared cache, a constant — and is a bug the rest of the time, in the same family as the mutable default argument.

__repr__ earns its keep immediately

By default, printing an object gives you something like <__main__.Point object at 0x104...>, which tells you nothing. Define __repr__ and you decide:

def __repr__(self):
    return f"Point({self.x}, {self.y})"

It costs two lines and pays for itself the first time you print a list of them.

When a class is the right answer

Not everything should be a class. Python is not a language where you have to wrap a function in one to run it, and a module full of functions is a perfectly good design.

The signal that you want a class is data and behaviour travelling together. If several functions all take the same three arguments, and those three arguments always change as a group, they are describing one thing that does not have a name yet. Giving it a name and hanging the functions off it as methods removes the repetition and makes the relationship explicit.

The opposite signal is a class with one method and no state beyond what that method was passed. That is a function wearing a costume, and the function is easier to test, easier to import and easier to read.

State is the thing to be careful with

An object's attributes are its state, and state is what makes code harder to reason about, because the answer to "what does this method return" becomes "it depends what happened earlier". That is not an argument against classes; it is an argument for keeping state small and obvious.

Two habits help. Set every attribute in __init__, even if only to None, so that reading the constructor tells you everything the object holds - attributes appearing halfway through some other method are how objects become mysterious. And prefer methods that return new values over methods that quietly mutate the object, unless mutation is the point and the name says so.

The underscore convention

Python has no private attributes. A leading underscore - self._cache - is a convention meaning "this is internal, do not rely on it", and nothing enforces it. That sounds weak and works surprisingly well: it marks the boundary between what you promise and what you may change, which is all the distinction was ever for.

A double leading underscore triggers name mangling, which is a different feature aimed at avoiding clashes in subclasses rather than at privacy. It is rarely what you want.

Dataclasses, for when it is mostly data

If a class exists chiefly to hold fields, the standard library will write the boilerplate:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

That gives you __init__, a readable __repr__ and equality comparison for free, which is three of the things you would otherwise write by hand and one - __repr__ - that people usually skip and then miss the first time they print a list of them.

A small class, end to end

Everything a plain class usually needs, in one block:

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError(f"deposit must be positive, got {amount}")
        self.balance += amount
        return self.balance

    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

    def __eq__(self, other):
        if not isinstance(other, Account):
            return NotImplemented
        return (self.owner, self.balance) == (other.owner, other.balance)


a = Account("ana")
a.deposit(50)

print(a)
print(a == Account("ana", 50))

try:
    a.deposit(-5)
except ValueError as e:
    print("ValueError:", e)
Account('ana', 50)
True
ValueError: deposit must be positive, got -5

Four things earn their place. Every attribute is set in __init__, so reading it tells you everything the object holds. The method validates its input and says what it received, not just that something was wrong. __repr__ uses !r on the owner so strings print quoted, which is what makes a repr unambiguous. And __eq__ returns NotImplemented — not False — for unrelated types, which lets Python try the other object's comparison before giving up.

The dunder methods worth knowing

Python's protocols are all spelled as double-underscore methods, and defining them is how your class gets to behave like a built-in type. Six cover most needs.

__repr__ is for programmers and should ideally look like the code that would recreate the object. __str__ is for users; if you only write one, write __repr__, because str() falls back to it and the interactive prompt and containers use it regardless.

__eq__ decides what equality means. Define it and you almost always want __hash__ too, because a class that defines __eq__ without __hash__ becomes unhashable and cannot go in a set or be a dictionary key. The pair must agree: objects that compare equal must hash the same.

__len__ gives len() and, as a side effect, truthiness. __iter__ makes the object work in a for loop, in unpacking, and in list(). __contains__ gives in, though __iter__ alone provides a working fallback.

The rest — arithmetic operators, context managers, comparisons — follow the same idea: Python asks the object, and the object answers by having the method. There is no separate interface to declare. A dataclass writes __init__, __repr__ and __eq__ for you, which is three of the six and the usual reason to reach for one.

Properties, for when an attribute needs logic

Python has no tradition of writing get_x() and set_x() for every field, because it does not need one. Attributes start as plain attributes, and if one later needs validation or computation, @property converts it without changing a single call site.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

c.area is now computed on access and written without brackets, so it reads like data. A matching @area.setter would let it be assigned, typically to validate: raising on a negative radius at the moment it is set rather than discovering it later in a calculation.

The important consequence is cultural. In languages where adding validation means changing every caller, people write accessors for everything up front, just in case. In Python that insurance is unnecessary, so the idiomatic style is a plain attribute until the day it needs to be more — and the day it does, nothing outside the class changes.

Use a property when the value is genuinely attribute-like: cheap, side-effect free, and conceptually part of the object's state. If it does real work, makes a network call, or can fail in interesting ways, a method with brackets is honest about that and a property is not.

What makes a class worth reading

The mechanics of a class are simple; the judgement is in what goes on it, and a few habits separate classes people can use from classes people work around.

One responsibility, named. If the class name needs "and" to describe it, it is two classes. A name that is a noun for a thing in the problem — Invoice, Connection, Basket — is a better sign than one built from patterns, like InvoiceManager or DataHandler, which usually means "some functions I put somewhere".

A small surface. The methods a caller needs should be few and obvious, and everything else should carry a leading underscore. A class with twenty public methods is asking its callers to learn twenty things, and most of them generally turn out to be steps of two or three real operations.

Attributes that are all set in one place. __init__ should establish every attribute the object will ever have. Attributes that appear halfway through another method make the object's state depend on call order, which is exactly the thing that turns into "it works if you call load() first".

**Methods that use self.** A method that never touches the instance is a function that has been filed in the wrong place. Moving it out makes it testable on its own and shortens the class.

The underlying question is whether the class makes calling code shorter and clearer than the equivalent functions would. If it does not, the functions were the right answer, and Python is perfectly happy with a module full of them.

Testing one

A class is easy to test when its state is small and its methods return values, and difficult when it is neither — which makes testability a useful design signal rather than a separate chore.

The straightforward shape is: construct the object, call a method, assert on what came back or on one attribute. If that is awkward, the reason is usually one of three things. The constructor does real work, such as opening a file or making a request, so the object cannot be built in a test without the outside world; the fix is to take the connection as an argument rather than creating it. The method returns nothing and changes several attributes, so the assertion has to know internals; the fix is usually to return the result. Or the outcome depends on what was called earlier, in which case the test has to replay a sequence, and that is the sign that the state wants to be smaller.

__eq__ earns its place here too: it lets a test compare a whole object against an expected one in a line, instead of asserting on four attributes and missing the fifth.

Questions people ask

Why does every method need self? Because a.speak() is Dog.speak(a) — the instance is passed explicitly rather than appearing by magic. Python prefers explicit.

What is the difference between a class and an instance attribute? A class attribute is one object shared by everything; an instance attribute belongs to one object. Assignment always creates the instance one.

Do I need getters and setters? No. Use plain attributes, and @property on the day one needs logic.

What is __slots__? A declaration of the allowed attributes that saves memory for classes with many instances, at the cost of losing dynamic attributes. Worth it rarely.

Should I write __str__ or __repr__? __repr__ first — it is what containers and the prompt use, and str() falls back to it.

What is a @staticmethod for? A function that belongs with the class conceptually but uses neither the instance nor the class. A module function is often just as good.

Why does my object print as an address? Because it has no __repr__. Two lines fix it permanently.

Can a class have no methods at all? Yes, and a dataclass with only fields is a perfectly good way to give a group of related values a name and a readable repr.

Recap in one screen

  • A class bundles data with the functions that operate on it; each instance carries its own attributes and shares the methods.
  • Set every attribute in __init__, so reading the constructor tells you what the object holds.
  • self is the instance, passed explicitly — a.f() is Cls.f(a).
  • Class attributes are shared by every instance; assignment through self always creates an instance attribute instead.
  • __repr__ costs two lines and pays for itself the first time you print a list of them; a dataclass writes it, __init__ and __eq__ for you.

Check yourself

0 of 3

Answer without scrolling back up.

  1. What is `self`?

  2. `total = 0` written directly in the class body is:

  3. You print an object and get `<__main__.Point object at 0x...>`. The fix?

Cheat sheet

Classes and Objects

A class bundles data with the functions that operate on it. The class is a template; each instance built from it carries its own copy of the data and shares the behaviour.

PYTHON · vizlearn.in/python/classes_and_objects.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.