Dataslope logoDataslope

Classes

Defining your own types with attributes, methods, and dataclasses

A class bundles data and behavior. Every value in Python is an instance of some class — 42 is an int, "hello" is a str, [1, 2, 3] is a list. Classes let you define your own types to model the problem you're solving.

Why classes matter: the real world

Classes are how you model entities in your domain:

  • Web frameworks — Django models, Flask views, FastAPI request/response objects
  • Data pipelines — Pandas DataFrame methods, SQLAlchemy table definitions
  • Games — Player, Enemy, Item, Inventory classes
  • APIs — User, Order, Payment, Invoice objects
  • Configuration — Settings classes, feature flags, environment wrappers

Every major Python library uses classes to organize behavior and state. Understanding classes is essential for reading and writing idiomatic Python.

Etymology: why 'class'?

The word "class" comes from classification — the practice of grouping things by shared characteristics. Linnaeus used classes in biological taxonomy (Kingdom, Phylum, Class, Order...). In programming, a class is a template for creating objects that share structure and behavior.

A minimal class

Code Block
Python 3.13.2

class Dog: defines a new type. Dog() creates an instance of that type. The class body can be empty (pass), but usually you add an __init__ method.

The __init__ method

__init__ is the initializer (not "constructor" — the object already exists when __init__ runs). It sets up a new instance.

Code Block
Python 3.13.2

When you call Dog("Rex", 4), Python:

  1. Creates a new, empty Dog instance.
  2. Calls __init__(d, "Rex", 4) to initialize it.
  3. Returns the initialized instance.

What is 'self'?

self is just a convention — the name is not special. The first parameter of an instance method always receives the instance, and by convention we name it self. You could call it this or me or obj, but everyone uses self, so you should too.

Instance attributes vs class attributes

An instance attribute belongs to a single object. A class attribute is shared by all instances.

Code Block
Python 3.13.2

Mutable class attributes are a gotcha

If a class attribute is a mutable object (list, dict, set), all instances share that single object. Modifying it in one instance affects all others. This is almost never what you want.

```python class Bad: items = [] # shared by all instances!

a = Bad() b = Bad() a.items.append(1) print(b.items) # [1] — oops! ```

Always initialize mutable per-instance data in `init`.

Methods

A method is a function defined inside a class. The first parameter is always the instance (self), and Python passes it automatically when you call instance.method().

Code Block
Python 3.13.2

When you write d.bark(), Python translates it to Dog.bark(d) — the instance is passed as the first argument.

Code Block
Python 3.13.2

@classmethod and @staticmethod

Sometimes you want a method that operates on the class itself, or a utility function that logically belongs to the class but doesn't need access to instances.

Code Block
Python 3.13.2
  • @classmethod receives the class as the first argument (cls). Useful for alternative constructors.
  • @staticmethod receives nothing automatic. It's just a function that lives in the class namespace for organizational reasons.

When to use @classmethod

Use @classmethod for alternative constructors — methods that create instances in different ways. Examples: dict.fromkeys(...), datetime.fromtimestamp(...), json.loads(...) (conceptually). The pattern is @classmethod returns cls(...).

Properties: computed attributes

Use @property to define a method that looks like an attribute.

Code Block
Python 3.13.2

Properties are useful when you want to compute a value on-the-fly or add logic (validation, logging) when reading/writing an attribute.

Dunder methods: special behavior

Methods with double underscores ("dunder") define how instances interact with Python operators and built-in functions.

DunderPurposeTriggered by
__init__Initialize instanceMyClass(...)
__repr__Unambiguous stringrepr(obj), REPL display
__str__User-friendly stringstr(obj), print(obj)
__eq__, __lt__, ...Comparisonsa == b, a < b
__len__Lengthlen(obj)
__iter__Iterationfor x in obj
__getitem__Indexingobj[key]
__call__Make instance callableobj()
Code Block
Python 3.13.2

__repr__ vs __str__

  • __repr__ should be unambiguous and ideally show how to reconstruct the object. Target audience: developers.
  • __str__ should be readable and friendly. Target audience: end users.

If you only define one, define __repr__. str(obj) falls back to repr(obj) if __str__ is missing.

Defining __eq__ without __hash__

If you define __eq__, Python sets __hash__ to None, making instances unhashable (can't be dict keys or set elements). If you want hashable instances, define both __eq__ and __hash__, or use @dataclass(frozen=True).

@dataclass: the easy mode

Writing __init__, __repr__, and __eq__ for every data-holding class is tedious. @dataclass generates them from type-annotated attributes.

Code Block
Python 3.13.2

@dataclass generates:

  • __init__ that takes x and y as arguments
  • __repr__ that shows Point(x=3, y=4)
  • __eq__ that compares all fields
Code Block
Python 3.13.2

Use @dataclass for simple records

If your class is mostly "a bundle of named fields", use @dataclass. It's less boilerplate and more readable than writing __init__ by hand. For complex behavior, a regular class is fine.

Multi-file challenges

Challenge
Python 3.13.2
Bank account with deposit and withdraw

Open account.py and implement the BankAccount class with:

  • __init__(self, owner, balance=0) — Store owner and balance.
  • deposit(amount) — Add to balance, return new balance. Raise ValueError if amount is negative.
  • withdraw(amount) — Subtract from balance, return new balance. Raise ValueError if amount is negative or exceeds balance.

main.py uses your class. Do not edit main.py.

Challenge
Python 3.13.2
Shopping cart with products

You'll implement two classes across two files:

product.py: Define Product with __init__(self, name, price) and a __repr__.

cart.py: Define ShoppingCart with:

  • __init__(self) — Initialize an empty list of items.
  • add(product) — Append a product.
  • total() — Return sum of all product prices.

main.py uses both. Do not edit main.py.

Challenge
Python 3.13.2
Dataclass Point

Open geometry.py and define a @dataclass named Point with two float fields: x and y.

main.py uses your class. Do not edit main.py.

Challenge
Python 3.13.2
A simple counter class

Define a class Counter with:

  • An __init__(self, start=0) constructor that stores the starting value in self.value.
  • A method increment(step=1) that adds step to self.value and returns the new value.
  • A method reset() that sets self.value back to 0.

Multiple choice questions

QuestionSelect one

What is the role of self in instance methods?

It is a keyword that Python requires in method definitions.

It is the conventional name for the first parameter, which receives the instance.

It refers to the class, not the instance.

It is automatically defined; you do not need to include it in method signatures.

QuestionSelect one

What does @dataclass generate for you?

Only __init__.

__init__, __repr__, and __eq__ (and optionally __hash__ if frozen=True).

Runtime type validation for all fields.

Private attributes for all fields.

QuestionSelect one

What is the difference between __repr__ and __str__?

They are synonyms; Python 3 renamed __str__ to __repr__.

__repr__ is unambiguous for developers; __str__ is user-friendly.

__repr__ is for built-in types; __str__ is for user classes.

__str__ is automatically generated; __repr__ must be written by hand.

QuestionSelect one

When should you use @classmethod instead of a regular method?

When the method does not need access to self.

When the method needs to access the class itself, often for alternative constructors.

When the method modifies a class attribute.

When the method is private.

QuestionSelect one

Why is defining __eq__ without __hash__ a problem?

Python will raise a SyntaxError.

Instances become unhashable and cannot be used as dict keys or in sets.

__eq__ comparisons will always return False.

The class becomes abstract and cannot be instantiated.

QuestionSelect one

What happens if a class attribute is a mutable object (e.g., a list) and you modify it in one instance?

Each instance gets its own copy of the list.

The change is visible in all instances because they share the same list object.

Python raises a TypeError.

The class attribute becomes an instance attribute.

Inheritance lets one class build on another. That is next.

On this page