DEV Community

Cover image for The Blueprint Room: Classes and Instances
Aaron Rose
Aaron Rose

Posted on

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

"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)
Enter fullscreen mode Exit fullscreen mode

"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)
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

"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!
Enter fullscreen mode Exit fullscreen mode

"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!
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

"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__
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)