The Blueprint Room: Classes and Instances
Timothy had been creating book records for weeks—each one a dictionary with title, author, year, and page count. The pattern was repetitive:
book1 = {"title": "Dune", "author": "Herbert", "year": 1965, "pages": 412}
book2 = {"title": "1984", "author": "Orwell", "year": 1949, "pages": 328}
book3 = {"title": "Foundation", "author": "Asimov", "year": 1951, "pages": 255}
He'd forgotten the "year" field on one book, misspelled "author" as "auther" on another, and had no way to add behavior—like calculating reading time or checking if a book was recent.
Margaret found him creating yet another dictionary. "You're hand-crafting each record," she observed. "Come with me to the Object-Oriented Manor—specifically, the Blueprint Room."
The Repetition Problem
Timothy's manual approach had issues:
# Creating books manually - prone to errors
book = {"title": "Dune", "author": "Herbert", "year": 1965}
# Oops, forgot pages!
# Different books might have inconsistent fields
another_book = {"title": "1984", "auther": "Orwell", "pages": 328}
# Typo: "auther" instead of "author"
# No way to add behavior
def calculate_reading_time(book_dict):
return book_dict["pages"] * 2 # 2 minutes per page
"You need a template," Margaret explained, opening a door to reveal a vast room filled with architectural drafting tables. "A blueprint that ensures every book has the right structure and capabilities."
The Class: A Blueprint for Objects
Margaret showed Timothy his first class:
class Book:
def __init__(self, title, author, year, pages):
self.title = title
self.author = author
self.year = year
self.pages = pages
"This is a blueprint," Margaret explained. "It defines what every Book object will have. The __init__
method is the initialization ritual—it runs whenever you create a new Book."
Timothy created his first instance:
# Create a Book instance
dune = Book("Dune", "Herbert", 1965, 412)
# Access its attributes
print(dune.title) # "Dune"
print(dune.author) # "Herbert"
print(dune.year) # 1965
print(dune.pages) # 412
"Each call to Book()
creates a new instance—a specific book built from the blueprint," Margaret noted. "The blueprint ensures every book has all required fields."
Understanding self
Timothy was puzzled by self
. "What is this parameter that appears everywhere but I never pass it?"
Margaret demonstrated:
class Book:
def __init__(self, title, author, year, pages):
# self refers to THIS specific instance being created
self.title = title
self.author = author
self.year = year
self.pages = pages
# When you call Book(...), Python automatically:
# 1. Creates a new empty object
# 2. Passes it as 'self' to __init__
# 3. Returns the initialized object
dune = Book("Dune", "Herbert", 1965, 412)
# Behind the scenes: __init__(dune, "Dune", "Herbert", 1965, 412)
"self
is like writing 'this book' in instructions," Margaret explained. "When you say self.title = title
, you're saying 'set THIS book's title to the provided title value.'"
Adding Methods: Behavior to the Blueprint
Timothy learned classes could do more than store data:
class Book:
def __init__(self, title, author, year, pages):
self.title = title
self.author = author
self.year = year
self.pages = pages
def get_reading_time(self):
minutes_per_page = 2
return self.pages * minutes_per_page
def is_recent(self, current_year=2025):
return (current_year - self.year) <= 10
# Use the methods
dune = Book("Dune", "Herbert", 1965, 412)
print(dune.get_reading_time()) # 824 minutes
print(dune.is_recent()) # False (published 1965)
hail_mary = Book("Project Hail Mary", "Weir", 2021, 476)
print(hail_mary.is_recent()) # True (published 2021)
"Methods are functions that belong to the class," Margaret explained. "They have access to the instance's data through self
."
Instance Attributes: Each Object's Unique Data
Timothy discovered each instance had its own data:
dune = Book("Dune", "Herbert", 1965, 412)
foundation = Book("Foundation", "Asimov", 1951, 255)
# Each instance maintains its own attributes
print(dune.title) # "Dune"
print(foundation.title) # "Foundation"
# Modifying one doesn't affect the other
dune.title = "Dune: Special Edition"
print(dune.title) # "Dune: Special Edition"
print(foundation.title) # "Foundation" - unchanged
"Each instance is independent," Margaret noted. "They share the blueprint but maintain their own state."
Class Attributes: Shared Data
Margaret showed Timothy attributes that belonged to the class itself:
class Book:
# Class attribute - shared by all instances
library_name = "The Grand Library"
total_books = 0
def __init__(self, title, author, year, pages):
self.title = title
self.author = author
self.year = year
self.pages = pages
# Increment class attribute
Book.total_books += 1
# All instances share class attributes
dune = Book("Dune", "Herbert", 1965, 412)
foundation = Book("Foundation", "Asimov", 1951, 255)
print(dune.library_name) # "The Grand Library"
print(foundation.library_name) # "The Grand Library"
print(Book.total_books) # 2
"Class attributes are like constants or shared state," Margaret explained. "Every instance can access them, but they belong to the blueprint, not to individual instances."
The Mutable Class Attribute Trap
Margaret warned Timothy about a dangerous mistake with class attributes:
class Book:
# DANGER: Mutable class attribute!
tags = [] # Shared by ALL instances
def __init__(self, title, author):
self.title = title
self.author = author
def add_tag(self, tag):
self.tags.append(tag) # Modifies the shared list!
dune = Book("Dune", "Herbert")
foundation = Book("Foundation", "Asimov")
dune.add_tag("scifi")
print(foundation.tags) # ['scifi'] - Foundation has Dune's tag!
"This is a common trap," Margaret cautioned. "Mutable class attributes like lists and dictionaries are shared across all instances. Changes to one affect all."
# CORRECT: Make it an instance attribute
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
self.tags = [] # Each instance gets its own list
dune = Book("Dune", "Herbert")
foundation = Book("Foundation", "Asimov")
dune.add_tag("scifi")
print(foundation.tags) # [] - Separate lists!
Attribute Shadowing
Timothy discovered instance attributes could shadow class attributes:
class Book:
library_name = "Grand Library" # Class attribute
def __init__(self, title):
self.title = title
book = Book("Dune")
print(book.library_name) # "Grand Library" (from class)
# Create instance attribute with same name
book.library_name = "Branch Library"
print(book.library_name) # "Branch Library" (instance)
print(Book.library_name) # "Grand Library" (class unchanged)
"The instance attribute shadows the class attribute," Margaret explained. "Python checks the instance first, then the class."
Dynamic Attributes and Privacy Conventions
Timothy discovered Python's flexibility—and conventions:
dune = Book("Dune", "Herbert", 1965, 412)
# Can add attributes after creation
dune.genre = "Science Fiction"
dune.rating = 5
print(dune.genre) # "Science Fiction"
# This works, but breaks the blueprint guarantee
"Python allows dynamic attributes," Margaret noted, "but it defeats the purpose of having a blueprint. Avoid it in production code."
She also showed him naming conventions for privacy:
class Book:
def __init__(self, title, isbn, secret_code):
self.title = title # Public - use freely
self._isbn = isbn # Protected - internal use
self.__secret_code = secret_code # Private - name mangling
book = Book("Dune", "978-0441013593", "ABC123")
print(book.title) # OK - public attribute
print(book._isbn) # Works, but signals "don't touch"
# print(book.__secret_code) # AttributeError - Python mangles the name
"Python doesn't enforce privacy," Margaret explained, "but these conventions signal intent. Single underscore means 'internal use.' Double underscore triggers name mangling for true privacy."
String Representation: str and repr
Timothy wanted better output when printing books:
class Book:
def __init__(self, title, author, year, pages):
self.title = title
self.author = author
self.year = year
self.pages = pages
def __str__(self):
# For end users - readable format
return f'"{self.title}" by {self.author} ({self.year})'
def __repr__(self):
# For developers - unambiguous, recreates object
return f'Book("{self.title}", "{self.author}", {self.year}, {self.pages})'
dune = Book("Dune", "Herbert", 1965, 412)
print(dune) # "Dune" by Herbert (1965) - uses __str__
print(repr(dune)) # Book("Dune", "Herbert", 1965, 412) - uses __repr__
"Two methods control string representation," Margaret explained. "__str__
creates readable output for users. __repr__
creates unambiguous output for developers—ideally, you could paste it to recreate the object."
Real-World Example: Library Card Catalog
Margaret showed Timothy a practical application:
class Book:
def __init__(self, title, author, year, pages, isbn):
self.title = title
self.author = author
self.year = year
self.pages = pages
self.isbn = isbn
self.is_checked_out = False
def checkout(self):
if self.is_checked_out:
return False
self.is_checked_out = True
return True
def return_book(self):
self.is_checked_out = False
def __str__(self):
status = "checked out" if self.is_checked_out else "available"
return f'"{self.title}" by {self.author} - {status}'
# Create catalog
catalog = [
Book("Dune", "Herbert", 1965, 412, "978-0441013593"),
Book("1984", "Orwell", 1949, 328, "978-0451524935"),
Book("Foundation", "Asimov", 1951, 255, "978-0553293357")
]
# Check out a book
catalog[0].checkout()
print(catalog[0]) # "Dune" by Herbert - checked out
When to Use Classes
Margaret emphasized when classes made sense:
Use classes when you have:
- Data that naturally groups together (books have titles, authors, pages)
- Behavior associated with that data (books can be checked out, returned)
- Multiple instances of the same type (many books in a library)
- State that changes over time (checkout status)
Don't use classes when:
- A dictionary or tuple would suffice
- You only need one instance
- The data has no associated behavior
- Simple functions would be clearer
Timothy's Class Wisdom
Through exploring the Blueprint Room, Timothy learned essential principles:
Classes are blueprints: They define structure and behavior for objects.
Instances are individual objects: Each has its own data but shares the blueprint.
init initializes instances: It runs when creating new objects.
self refers to the instance: It lets methods access the object's own data.
Instance attributes are unique: Each object maintains its own state.
Class attributes are shared: All instances access the same value.
Mutable class attributes are dangerous: Lists or dicts as class attributes are shared across all instances—use instance attributes instead.
Instance attributes shadow class attributes: Python checks the instance first, then the class.
Methods add behavior: Functions that work with the object's data.
str for users, repr for developers: __str__
creates readable output; __repr__
creates unambiguous output.
Privacy is by convention: Single underscore (_attr
) signals internal use; double underscore (__attr
) triggers name mangling.
Dynamic attributes are possible: But they break the blueprint guarantee—avoid in production.
Classes organize related data and behavior: They create custom types.
Not everything needs a class: Simple data often works better as dictionaries or tuples.
Timothy had discovered the power of blueprints—the ability to create custom types with their own data and behavior. The Object-Oriented Manor would reveal even more sophisticated patterns, but the Blueprint Room had given him the foundation: classes as templates for creating consistent, capable objects in his ever-expanding library system.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Comments
Post a Comment