BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

PY-01-017-P · Primer — Classes & Objects

The idea in one sentence

A class is a blueprint that bundles related data (attributes) and the actions that work on it (methods) into one type, and each object you create from it is a self-contained instance that carries its own data and knows how to act on it.

Why it exists

Once a program grows past a handful of variables, you start noticing that certain values always travel together and certain functions always operate on them: a user has a name, an email, and a password, and you keep passing all three into send_welcome(name, email), reset_password(email, password), and so on. Passing loose variables around gets fragile fast — nothing stops you from mixing up one user's email with another user's name, and there's no single place that says "here is what a user is and what a user can do." A class fixes exactly this: it lets you define a new type — User, BankAccount, ShoppingCart — that glues the data and the behavior together, so the thing that owns the data is the same thing that acts on it. Each object made from the class keeps its own copy of that data, so a thousand accounts can share one set of rules while each remembers its own balance. This is the leap from writing steps to modeling things, and it's how nearly every real Python library — from web frameworks to data tools — is organized.

How it works

You define a class with the class keyword and a name in CapWords style, then create objects by calling the class like a function: acct = BankAccount("Ada"). The special method __init__ (the initializer, said "dunder init") runs automatically at that moment and sets up the object's starting instance attributes — the per-object data — by assigning to self.something. That first parameter, self, is Python's name for the particular object being worked on; you write it in every method signature, but you don't pass it yourself — acct.deposit(100) quietly becomes BankAccount.deposit(acct, 100). A method is just a function defined inside the class whose first job is to read or change self's attributes. By naming an attribute with a leading underscore (self._balance), you signal "this is internal — touch it through methods, not directly," which is Python's gentle, convention-based encapsulation.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner          # public instance attribute
        self._balance = balance     # "internal" by convention

    def deposit(self, amount):      # method: self is THIS account
        self._balance += amount
        return self._balance

    def balance(self):
        return self._balance

acct = BankAccount("Ada")           # __init__ runs here, self = acct
acct.deposit(100)                   # same as BankAccount.deposit(acct, 100)
acct.deposit(50)
print(acct.owner)                   # Ada
print(acct.balance())               # 150
print(isinstance(acct, BankAccount))# True

Each BankAccount you create gets its own owner and _balance, so two accounts never step on each other's data.

The one gotcha

Forgetting self — either in a method's parameter list or when touching an attribute — is the mistake every beginner makes. Inside a method, a bare balance is a local variable that vanishes when the method ends; only self.balance reaches the attribute that lives on the object. And every method must list self as its first parameter, or Python will complain that you passed one too many arguments the moment you call it:

class Counter:
    def __init__(self):
        self.n = 0

    def bump(self):        # self is required here...
        self.n += 1        # ...and here, or "n" would be a lost local

c = Counter()
c.bump()
c.bump()
print(c.n)                 # 2   (self.n persisted between calls)

The rule to burn in: read and write per-object data through self, and start every method with self. If a method truly ignores the object, it probably shouldn't be a method at all — it can just be a plain function.

See it — how acct.deposit(50) finds its data

flowchart TD
    A["acct = BankAccount('Ada')"] --> B["__init__ runs<br/>self.owner = 'Ada'<br/>self._balance = 0"]
    B --> C["acct now holds<br/>its own attributes"]
    C --> D["You call<br/>acct.deposit(50)"]
    D --> E["Python rewrites it as<br/>BankAccount.deposit(acct, 50)"]
    E --> F["Inside: self IS acct,<br/>amount is 50"]
    F --> G["self._balance += amount<br/>updates THIS account only"]
    G --> H["Other accounts'<br/>balances untouched"]

The one-line takeaway

A class bundles data and behavior into a new type; __init__ sets up each object's own attributes, and every method uses self to read and change the data of the specific object it was called on.

Where you'll meet it

Classes are everywhere the moment you leave tiny scripts behind. You'll define your own to model the nouns of your program — a User, an Order, a Deck of cards — bundling their fields in __init__ and their rules in methods so the logic lives next to the data it protects. Far more often, you'll use classes other people wrote: nearly every library hands you objects and expects you to call their methods. When you write response = requests.get(url) and then read response.status_code or call response.json(), you're touching an object's attributes and methods; a Flask or Django app is a web of your classes talking to the framework's; a pandas DataFrame, a datetime, a file handle — all objects. Recognizing something.attribute and something.method() as "reaching into an object" is a daily reflex for a Python programmer, and understanding self is what lets you finally read library source code and build the well-organized, reusable pieces that scale past a single file.

Named concepts: class definitions and instances, init and instance attributes, methods and self, basic encapsulation