Skip to main content
Python Basics2026-07-29 Β· 12 min read

Python Classes and Objects: The 2026 Tutorial

Most Python OOP tutorials teach the syntax and skip the mental model. This one does the opposite β€” you'll leave with a clear picture of what a class is at runtime, when to reach for @dataclass instead of writing __init__ by hand, and how self really works (it's not magic).

We'll cover: the four-line minimum viable class, why self is a parameter, class vs instance attributes (the #1 beginner bug), the six dunder methods you'll see everywhere, @dataclass as the modern default, and a hard rule for when to leave classes alone and use a plain function.

The mental model: a class is a factory

A class in Python is a factory that produces objects. Every time you call Rect(3, 4), Python creates a fresh object with its own storage and hands it back to you.

β–Ά PYTHONeditable Β· runs in your browser

That's the entire mechanic. The class is the blueprint; each Rect(...) call is a stamp of the blueprint into fresh RAM.

self is just a parameter

The #1 confusion for people coming from JS or Ruby: self in Python is explicit. Every method's first parameter is a reference to the instance being called.

β–Ά PYTHONeditable Β· runs in your browser

So c.bump() is syntactic sugar for Counter.bump(c). Python passes the instance as the first arg. You could call it me, this, or foo β€” self is convention, not a keyword. Stick with self (PEP 8) so other Python developers can read your code.

The class vs instance attribute trap

The most common Python OOP bug in a beginner codebase:

β–Ά PYTHONeditable Β· runs in your browser

items = [] at class scope creates ONE list shared by every instance. a.add("milk") mutates it, so b.items sees the same list.

Rule: initialise mutable state in `__init__`, not at class scope.

β–Ά PYTHONeditable Β· runs in your browser

This bug shipped to production more times than any Python developer will admit. Watch for lists, dicts, sets at class scope.

The six methods you'll see everywhere

Dunder methods (__name__) are Python's operator overloading hooks. These six cover 95% of what you'll encounter:

1. __init__(self, ...) β€” constructor

Runs when the object is created. Set instance attributes here. Don't return anything (Python enforces None).

2. __repr__(self) β€” developer-facing string

Called by print(x) if __str__ is missing, and always by the REPL. Aim for something you could paste back into Python to recreate the object:

β–Ά PYTHONeditable Β· runs in your browser

3. __eq__(self, other) β€” equality

Without it, Rect(3, 4) == Rect(3, 4) returns False (default is identity). Compare field-by-field:

β–Ά PYTHONeditable Β· runs in your browser

4. __hash__(self) β€” hashability

If you override __eq__, you MUST also override __hash__ or Python sets __hash__ = None (object becomes unhashable). Simplest: return hash((self.width, self.height)).

5. __len__(self) β€” for len(obj)

Makes len(cart) == 5 work.

6. __iter__(self) β€” for for x in obj:

Return an iterator. yield inside makes the class iterable:

β–Ά PYTHONeditable Β· runs in your browser

@dataclass β€” the modern default

If your class is mostly a data holder (three fields, an __init__, a __repr__, an __eq__), stop writing that boilerplate. @dataclass generates it all from your type annotations:

β–Ά PYTHONeditable Β· runs in your browser

That's __init__, __repr__, and __eq__ all generated. Add @dataclass(frozen=True) to make the object immutable AND get __hash__ free.

Rule: reach for `@dataclass` first. Write a hand-rolled class only when you need methods that aren't attribute accessors.

__slots__ β€” memory optimisation (advanced)

By default every instance carries a __dict__ so you can add arbitrary attributes. __slots__ swaps that dict for a fixed C-level struct β€” smaller RAM footprint and faster attribute access:

β–Ά PYTHONeditable Β· runs in your browser

Use __slots__ when: (a) you're creating millions of instances and RAM matters, or (b) you want to catch typo'd attribute assignment at runtime. Otherwise skip it β€” it disables pickle by default and breaks multiple inheritance in subtle ways.

When to leave classes alone

Hard rule: if your class has ONE method plus `__init__`, it should probably be a function.

β–Ά PYTHONeditable Β· runs in your browser

Classes earn their weight when they hold state across multiple method calls, or when they implement a well-defined protocol (__iter__, __enter__, __eq__, etc.). One-shot logic is a function.

Inheritance β€” use it sparingly

Inheritance is real but over-used. In 2026 Python, prefer composition β€” hold another object as an attribute instead of inheriting from it:

β–Ά PYTHONeditable Β· runs in your browser

Inherit only when you have a genuine "is-a" relationship AND you want to override behaviour. A NamedRect that inherits from Rect to add a name field is fine. Building a five-level inheritance hierarchy for a business-logic class is code you'll be untangling in 6 months.


Classes are one of Python's most powerful features, and one of the easiest to over-use. The mental model that helps: a class is a factory for objects, self is a plain parameter, mutable state goes in __init__, reach for @dataclass first, and drop back to a function whenever the class has less than two state-carrying methods.

Next step: 30 hands-on OOP lessons covering classes, @dataclass, inheritance vs composition, and the dunder methods β€” with real Python running in your browser and an AI mentor on every step.

Ready to try it?

15 lessons free after a free signup β€” no card. Sample the platform, then decide.

Start free β†’

Or unlock every lesson

Pro β€” $12/mo, unlimited AI, cancel any time. Refund within 14 days worldwide.

See Pro plans β†’

Get one Python lesson + one career idea every Friday

No spam, no "buy our course now". Three bullets, every Friday. Unsubscribe with one click.