Skip to content
VLSI Mentor

Python Part I · Module 16 · Object-Oriented Programming

Your First Class: Grouping Data and Behaviour

Chapter 16.1 established that every value you have used belongs to a class that decides what it can do. This chapter writes one. The syntax is small — a header line, a method with a peculiar name, and an equally peculiar first parameter — and the first useful thing it buys you is not the ability to architect anything. It is that the source of pathlib, subprocess and pytest stops looking like a foreign language, because all three are built from exactly the shape on this page.

Intermediate15 min readPythonPart IFundamentalsOOPClasses

Part I · Module 16 · Chapter 16.2 · Your First Class: Grouping Data and Behaviour

1. The Problem It Solves

A regression run produces four facts about every test: its name, its seed, whether it passed, and how long it took. Without a class you keep them in parallel lists:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
names = ["smoke", "cdc", "burst"]
seeds = [1234, 5678, 9012]
passed = [True, False, True]
runtimes = [182.4, 204.1, 199.8]

for i in range(len(names)):
    print(f"{names[i]:<7} seed={seeds[i]:<5} passed={passed[i]:<5} {runtimes[i]}s")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke   seed=1234  passed=1     182.4s
cdc     seed=5678  passed=0     204.1s
burst   seed=9012  passed=1     199.8s

That works, and it keeps working for exactly as long as nobody touches the lists. The structure is held together by one unwritten rule — index i means the same test in all four lists — which nothing in the program states, checks or enforces. Section 6 shows what a single sort does to it.

There is also a smaller tell in that output: passed printed as 1 and 0 rather than True and False, because the width specifier formats a bool as the integer it is — True is 1. Four loose lists make that easy to miss; a named field makes it obvious.

2. The Shape

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class RunResult:
    def __init__(self, test, seed):
        self.test = test
        self.seed = seed


r = RunResult("smoke", 1234)
print(r.test, r.seed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke 1234

Four lines of definition and one of use. Each piece:

  • class RunResult: — introduces a new class and names it. Class names are CapWords by convention (Chapter 2.4), which is how you tell RunResult from a function at a glance.
  • def __init__(self, test, seed): — a method Python calls automatically when you create an instance.
  • self.test = test — stores a value on this object, under the name test.
  • RunResult("smoke", 1234) — creates one instance. Note you do not pass self; Python supplies it.

__init__ and self are introduced here as the shape you type. Chapter 16.3 is about why they are shaped that way, including the executed proof that self is nothing more than the instance arriving as the first argument.

3. Reading It Back

The attributes are ordinary names on an ordinary object, reachable with the dot from Chapter 16.1:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class RunResult:
    def __init__(self, test, seed):
        self.test = test
        self.seed = seed


r = RunResult("smoke", 1234)

print("test     :", r.test)
print("seed     :", r.seed)
print("type     :", type(r).__name__)
print("callable :", callable(r.test))
print("__dict__ :", r.__dict__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test     : smoke
seed     : 1234
type     : RunResult
callable : False
__dict__ : {'test': 'smoke', 'seed': 1234}

r.test is an attribute by Chapter 16.1's test — data, not callable.

__dict__ is the object's own storage, and seeing it demystifies a great deal: an instance is, at bottom, a dictionary with a class attached.

4. A Class Statement Creates Nothing

The most useful early correction. The class block is a definition, and it runs once, when Python reads the file:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print("before the class statement")


class RunResult:
    print("  the class body runs once, now")

    def __init__(self, test):
        print("  __init__ runs per instance:", test)
        self.test = test


print("after the class statement -- no instances exist yet")
a = RunResult("smoke")
b = RunResult("cdc")
print("two instances:", a.test, b.test)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before the class statement
  the class body runs once, now
after the class statement -- no instances exist yet
  __init__ runs per instance: smoke
  __init__ runs per instance: cdc
two instances: smoke cdc

The class body ran once, at definition time, before any instance existed. __init__ ran twice, once per creation.

A class is a factory: a description of how to build objects of a kind, not an object of that kind itself. That distinction is what Chapter 16.4 is built on.

5. Instances Are Separate

Each instance gets its own storage, which is the whole point:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class RunResult:
    def __init__(self, test, seed):
        self.test = test
        self.seed = seed


a = RunResult("smoke", 1234)
b = RunResult("cdc", 5678)

a.seed = 9999

print("a:", a.test, a.seed)
print("b:", b.test, b.seed)
print("same object:", a is b)
print("a.__dict__:", a.__dict__)
print("b.__dict__:", b.__dict__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a: smoke 9999
b: cdc 5678
same object: False
a.__dict__: {'test': 'smoke', 'seed': 9999}
b.__dict__: {'test': 'cdc', 'seed': 5678}

Changing a.seed left b untouched. Two calls to RunResult(...) produce two independent objects — unlike Chapter 5.8's list aliasing, where two names could refer to one value.

6. Why Bother: the Records Stay Together

Back to Section 1. Parallel lists are fine until something reorders one of them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
names = ["smoke", "cdc", "burst"]
runtimes = [182.4, 204.1, 199.8]

runtimes.sort()

for i in range(len(names)):
    print(f"{names[i]:<7} {runtimes[i]}s")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke   182.4s
cdc     199.8s
burst   204.1s

cdc now claims burst's runtime. One sort on one list, and every record is wrong. No exception, no warning — the lists are still the same length, which is the only invariant anything was checking.

With a class the fields travel together and cannot be separated:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class RunResult:
    def __init__(self, test, runtime_s):
        self.test = test
        self.runtime_s = runtime_s


results = [
    RunResult("smoke", 182.4),
    RunResult("cdc", 204.1),
    RunResult("burst", 199.8),
]

results.sort(key=lambda r: r.runtime_s)

for r in results:
    print(f"{r.test:<7} {r.runtime_s}s")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke   182.4s
burst   199.8s
cdc     204.1s

Sorted by runtime, and every name still has its own number. The grouping is the feature. (key=lambda r: r.runtime_s is Chapter 11.14's lambda, now reaching into an attribute.)

7. Reading a Real Class

The canonical purpose of this chapter is that library source stops being opaque. pathlib.Path is a class you have used since Chapter 15.8:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from pathlib import Path

p = Path("runs/seed_41/sim.log")

print("class     :", type(p).__name__)
print("is a class:", isinstance(Path, type))
print("attribute :", p.name)
print("method    :", p.is_absolute())
print("storage   :", "name" in dir(p), "exists" in dir(p))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class     : PosixPath
is a class: True
attribute : sim.log
method    : False
storage   : True True

Path is a class; p is an instance of PosixPath, a class built on Path for this operating system — which is Chapter 16.9's inheritance, visible in the wild before it has been taught.

Everything you have been doing with p is what Section 3 did with RunResult: look up a name on an instance, get either data or something callable.

8. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def __init__(self, name):
        self.name = name


a = Job("smoke")
b = Job("smoke")

print("A:", a.name == b.name)
print("B:", a is b)
print("C:", a == b)
print("D:", type(a) is type(b))

a.extra = "added later"
print("E:", a.__dict__)
print("F:", b.__dict__)
print("G:", hasattr(b, "extra"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: True
B: False
C: False
D: True
E: {'name': 'smoke', 'extra': 'added later'}
F: {'name': 'smoke'}
G: False

A and B — equal contents, different objects.

C is False, and it surprises people: two objects with identical attributes are not equal by default. Python compares identity unless the class says otherwise, which is Chapter 16.8's __eq__ and, more practically, Chapter 16.15's dataclass — where equality by field value comes for free.

D — both are the same class, even though they are not the same object.

E, F and G show that an attribute can be attached to one instance after creation, and that doing so affects no other instance. a now has a field b does not. It is legal, it is occasionally useful, and it is almost always a sign that the class should have declared the field in __init__ from the start — because a reader of the class body now has no way to know extra exists.

9. Exercises

Exercise 1 — the shape. Write a class with __init__ taking three values and storing them. Create two instances and print all six attributes.

Exercise 2 — the factory. Put a print in the class body and one in __init__. Create three instances and count how many times each fires.

Exercise 3 — separate storage. Create two instances, mutate one, and show with __dict__ that the other is untouched.

Exercise 4 — parallel lists. Reproduce Section 6's sort bug with four parallel lists. Then fix it with a class and confirm the records hold together.

Exercise 5 — read pathlib. For a Path object, find three attributes and three methods using dir() and callable(). Say which are data and which are behaviour.

Exercise 6 — equality. Create two instances with identical attributes and compare them with == and is. Explain both results.

Exercise 7 — late attributes. Add an attribute to one instance after creation. Show it is absent from another, and say why you would avoid this.

Exercise 8 — read a real one. Open the source of a standard-library class (from pathlib import Path; Path.__module__ will tell you where). Find its __init__ or equivalent and identify what it stores.

10. Summary

  • A class groups data that belongs together so it cannot drift apart.
  • class Name: defines a factory, not an object; the class body runs once.
  • __init__ runs per instance, and stores values with self.x = ....
  • You do not pass self — Python supplies it.
  • Class names use CapWords, which distinguishes them from functions.
  • An instance's storage is a dictionary, visible as __dict__.
  • Instances are independent: mutating one does not affect another.
  • Parallel lists come apart silently the moment one is reordered; grouped fields cannot.
  • Two instances with equal attributes are not == by default — Python compares identity unless told otherwise.
  • The first payoff is reading library source, where pathlib, subprocess and pytest are built from exactly this shape.

The next chapter explains the two pieces of syntax this one asked you to accept.

Where this fits

Part of the Python curriculum.