Object-Oriented Programming
A Minecraft Villager has stuff he carries (a profession, a level, a list of trades) and stuff he can do (sleep at night, restock at the workbench, breed with another villager). The stuff he carries is data. The stuff he can do is behavior. A class is the recipe that says every Villager you spawn gets the same kind of data and the same kind of behavior. An object is one actual villager, standing in your world, with his own name and his own trades.
The idea was born in 1962 in a basement at the Norwegian Computing Center in Oslo. Two researchers, Ole-Johan Dahl and Kristen Nygaard, were trying to write simulations — ships moving through a port, cars moving through a city. The languages of the day made you keep one giant pile of data on one side and one giant pile of functions on the other. To simulate one ship, you reached into the data pile, found the ship's row, and called a function on it. The bigger the simulation got, the more the two piles fought each other. Dahl and Nygaard's fix was to glue the data and the functions together into a single thing they called an object. The language they built was Simula 67. Ten years later, at Xerox PARC in California, a researcher named Alan Kay watched a demo of Simula and walked out with the idea for Smalltalk, where every value in the language is an object that talks to other objects by sending messages. Python's class system is the great-grandchild of that conversation.

You have used the conversational version of this since the very first lesson. We called them "things" with "features" they carry and "actions" they can do. The proper names: object, attribute, method. An object is one specific thing in memory — this villager, that card, this hand of cards. An attribute is a piece of data attached to that object. A method is a function attached to that object. Saying villager.level reads an attribute. Saying villager.restock() calls a method. The dot is how you reach inside the object.
The cleanest way to learn this is to build something we are going to use for the rest of the section: a deck of playing cards. A card has a rank (Ace, 2, 3, … Jack, Queen, King) and a suit (Spades, Hearts, Diamonds, Clubs). Every card in the world has those two pieces of data. A deck has 52 cards and knows how to shuffle and deal. A poker game, a few lessons from now, will lean on this exact shape.
Open a new file called cards.py in your venv and type this in. Type it. Don't paste.
import random
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
SUITS = ["s", "h", "d", "c"]
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
class Deck:
def __init__(self):
self.cards = [Card(r, s) for s in SUITS for r in RANKS]
def shuffle(self):
random.shuffle(self.cards)
def deal(self, n):
dealt = self.cards[:n]
self.cards = self.cards[n:]
return dealt
deck = Deck()
deck.shuffle()
hand = deck.deal(5)
print(hand)Run it with python cards.py. The output is ugly:
[<__main__.Card object at 0x10489f5d0>, <__main__.Card object at 0x10489f610>, <__main__.Card object at 0x10489f650>, <__main__.Card object at 0x10489f690>, <__main__.Card object at 0x10489f6d0>]Five cards, five hexadecimal addresses, zero information. Python printed the memory address of each Card object because nobody told it what a Card should look like when written down. The fix is a method called __repr__ — two underscores on each side. Python calls it a "dunder" method, short for "double underscore." The interpreter looks for __repr__ on any object you hand to print and uses whatever string it returns. Add this method inside Card:
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __repr__(self):
return f"{self.rank}{self.suit}"Run the file again:
[As, Kh, 9c, 4d, Js]That is the same hand. Same memory addresses. Same objects. The only thing that changed is that you taught the class how to describe itself. Every dunder method is a hook into Python's grammar. __repr__ controls how the object prints. __eq__ controls what == does. __len__ controls what len(obj) returns. Add the other two and watch the language wake up around your class:
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __repr__(self):
return f"{self.rank}{self.suit}"
def __eq__(self, other):
return self.rank == other.rank and self.suit == other.suit
class Deck:
def __init__(self):
self.cards = [Card(r, s) for s in SUITS for r in RANKS]
def __repr__(self):
return " ".join(repr(c) for c in self.cards)
def __len__(self):
return len(self.cards)
def shuffle(self):
random.shuffle(self.cards)
def deal(self, n):
dealt = self.cards[:n]
self.cards = self.cards[n:]
return dealt
deck = Deck()
print(len(deck))
print(Card("A", "s") == Card("A", "s"))
deck.shuffle()
print(deck.deal(5))The output reads like English now:
52
True
[As, Kh, 9c, 4d, Js]Without __eq__, two Ace of Spades objects are different things to Python — different memory addresses, different objects. With __eq__, you taught Python what "the same card" means in your game. Without __len__, len(deck) raises an error. With it, the deck behaves like a list when you ask its size. The class is no longer a stranger to the language. It speaks Python.
A small question for the reader: in the output above, after deck.shuffle() and deck.deal(5), how many cards are still in the deck?
The answer is 47. The deck started at 52, the deal removed 5 from the front of the list, the rest stayed put. Run print(len(deck)) after the deal to confirm.

Real card games are not all the same deck. Pinochle, a game your grandparents probably know, uses a 48-card deck made of two copies of the cards 9, 10, Jack, Queen, King, Ace in each suit. The shuffle and deal logic is identical. Only the starting cards differ. Writing a second class from scratch would mean copying the shuffle and deal methods, which is the kind of duplication that bites you the first time you fix a bug in one and forget the other. The fix is inheritance. You write a child class that says "I am a Deck, except for this one thing," and you only override the one thing.
class PinochleDeck(Deck):
def __init__(self):
pinochle_ranks = ["9", "10", "J", "Q", "K", "A"]
self.cards = [Card(r, s) for s in SUITS for r in pinochle_ranks for _ in range(2)]
pdeck = PinochleDeck()
print(len(pdeck))
pdeck.shuffle()
print(pdeck.deal(12))Output:
48
[Qs, 9h, Kc, As, 10d, Jh, 9s, Kd, Ac, Qh, Jc, 10s]PinochleDeck did not redefine shuffle, deal, __repr__, or __len__. Those came down from the parent class for free. The only thing the child overrode was __init__, because the starting cards differ. This is the whole point of inheritance: say what is different, inherit what is the same. Alan Kay's original Smalltalk demo at PARC in 1973 was a screen full of small overlapping windows, each one an object that inherited its scrollbar and its title bar from a parent class and only redefined what made it special. That demo became the Macintosh in 1984 and Windows in 1985. Every desktop you have ever used is built on the same trick you just saw with PinochleDeck.
Two more pieces complete the basic toolkit. @property lets you write a method that you call without parentheses, so it looks like an attribute. Useful when a value is computed but reads like data. @classmethod lets you write a method that belongs to the class itself, not to one instance — the standard use is an alternate constructor.
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __repr__(self):
return f"{self.rank}{self.suit}"
def __eq__(self, other):
return self.rank == other.rank and self.suit == other.suit
@property
def is_face(self):
return self.rank in ("J", "Q", "K")
@classmethod
def from_string(cls, text):
return cls(text[:-1], text[-1])
king = Card.from_string("Kh")
print(king)
print(king.is_face)Output:
Kh
TrueCard.from_string("K♥") builds a Card without calling Card("K", "♥") directly. The cls argument is the class itself, passed in for free by the @classmethod decorator, which means a subclass like PinochleDeck's cards would build the right kind of thing if they overrode it. king.is_face reads like an attribute but runs the method underneath. The class is starting to feel like a small machine you designed.
Classes hold state. State is also where bugs hide — the moment you have an object that can change, you have to track who changed it, when, and whether the change broke any of the methods that depend on it. The next lesson asks the opposite question: what if you wrote your code so the state never changed at all?