Lesson Overview
Understanding the difference between instance variables and class variables is crucial for effective Object-Oriented Programming in Python. These two types of variables serve different purposes and behave differently, and mixing them up can lead to subtle bugs. In this session, we'll explore both types in depth, learn when to use each, and examine common pitfalls to avoid.
The Fundamental Difference
Let's start with the basic definitions:
- Instance Variables: Variables that are unique to each instance (object) of a class
- Class Variables: Variables that are shared among all instances of a class
Real-world analogy: Think of a car manufacturing company. Every car (instance) has its own unique VIN number, color, and mileage (instance variables). However, all cars share the same manufacturer name, company logo, and warranty policy (class variables).
Let's see this distinction in Python code:
class Car:
# Class variable - shared by all instances
manufacturer = "TechAuto Inc."
def __init__(self, model, color):
# Instance variables - unique to each instance
self.model = model
self.color = color
self.mileage = 0
# Creating car instances
car1 = Car("Sedan", "Blue")
car2 = Car("SUV", "Red")
# Accessing instance variables
print(car1.model) # Output: Sedan
print(car2.model) # Output: SUV
print(car1.color) # Output: Blue
print(car2.color) # Output: Red
# Accessing class variable
print(car1.manufacturer) # Output: TechAuto Inc.
print(car2.manufacturer) # Output: TechAuto Inc.
print(Car.manufacturer) # Output: TechAuto Inc.
As you can see:
- The
manufacturerclass variable is defined directly inside the class but outside any methods - The
model,color, andmileageinstance variables are defined inside the__init__method and prefixed withself. - Each car has its own unique model and color, but they all share the same manufacturer
Where and How to Define Variables
Class Variables
Class variables are defined at the class level, outside of any method. They're typically placed at the top of the class definition for visibility:
class Student:
# Class variables
school_name = "Python High"
total_students = 0
def __init__(self, name, grade):
self.name = name
self.grade = grade
Student.total_students += 1 # Updating a class variable
Instance Variables
Instance variables are typically defined in the __init__ method, but they can also be created in any other instance method. They must be prefixed with self. to associate them with the instance:
class Student:
school_name = "Python High"
def __init__(self, name, grade):
# Instance variables defined in __init__
self.name = name
self.grade = grade
self.enrolled = True
def update_grade(self, new_grade):
# Instance variable modified in another method
self.grade = new_grade
def add_attendance(self, date, present):
# Creating a new instance variable dynamically
if not hasattr(self, 'attendance'):
self.attendance = {}
self.attendance[date] = present
Key point: Instance variables can actually be created anywhere an instance method has access to self, not just in __init__. This is one of Python's flexible but potentially confusing features.
Accessing Variables
Accessing Class Variables
Class variables can be accessed through:
- The class itself:
ClassName.variable - Any instance of the class:
instance.variable
Accessing Instance Variables
Instance variables can only be accessed through an instance: instance.variable
class Counter:
# Class variable
total_counters = 0
def __init__(self, start=0):
# Instance variable
self.count = start
Counter.total_counters += 1
# Create counters
counter1 = Counter()
counter2 = Counter(10)
# Access class variable
print(Counter.total_counters) # Output: 2
print(counter1.total_counters) # Output: 2
# Access instance variables
print(counter1.count) # Output: 0
print(counter2.count) # Output: 10
# This would raise an AttributeError
# print(Counter.count) # Error: type object 'Counter' has no attribute 'count'
Explanation: We can access total_counters through both the class and instances because it's a class variable. But we can only access count through instances because it's an instance variable specific to each counter.
Modifying Variables
The behavior when modifying variables can sometimes be surprising and is important to understand:
Modifying Class Variables
To modify a class variable properly, you should access it through the class name:
class BankAccount:
# Class variable
interest_rate = 0.02 # 2% interest rate for all accounts
def __init__(self, owner, balance=0):
# Instance variables
self.owner = owner
self.balance = balance
# Create accounts
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 2000)
# Access class variable
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
# Modify class variable - affects all instances
BankAccount.interest_rate = 0.03
print(account1.interest_rate) # Output: 0.03
print(account2.interest_rate) # Output: 0.03
The Instance Variable Shadow Effect
Here's where things get tricky. If you modify a class variable through an instance, Python creates a new instance variable with the same name, which "shadows" the class variable:
# Continuing from the previous example
# Modify through an instance (creates instance variable)
account1.interest_rate = 0.04
# Now account1 has its own interest_rate instance variable
print(account1.interest_rate) # Output: 0.04
print(account2.interest_rate) # Output: 0.03 (still using class variable)
print(BankAccount.interest_rate) # Output: 0.03 (class variable unchanged)
# Modify class variable again
BankAccount.interest_rate = 0.05
print(account1.interest_rate) # Output: 0.04 (using instance variable)
print(account2.interest_rate) # Output: 0.05 (using updated class variable)
Important warning: When you access instance.class_variable, Python first looks for an instance variable with that name. If not found, it looks for a class variable. If you assign a value to instance.class_variable, you create an instance variable that has priority over the class variable.
Use Cases for Class Variables
Class variables are particularly useful in several scenarios:
1. Keeping count of instances
class User:
user_count = 0
def __init__(self, username):
self.username = username
User.user_count += 1
@classmethod
def display_user_count(cls):
return f"Total users: {cls.user_count}"
# Creating users
user1 = User("alice")
user2 = User("bob")
user3 = User("charlie")
print(User.display_user_count()) # Output: Total users: 3
2. Sharing constants among all instances
class Circle:
PI = 3.14159 # Mathematical constant shared by all circles
def __init__(self, radius):
self.radius = radius
def area(self):
return Circle.PI * self.radius ** 2
def circumference(self):
return 2 * Circle.PI * self.radius
circle1 = Circle(5)
circle2 = Circle(10)
print(circle1.area()) # Output: 78.53975
print(circle2.circumference()) # Output: 62.8318
3. Default values that can be overridden per instance
class EmailClient:
server = "smtp.example.com" # Default server for all instances
port = 587 # Default port
use_ssl = True # Default SSL setting
def __init__(self, username, password, **kwargs):
self.username = username
self.password = password
# Override class defaults with any provided kwargs
if 'server' in kwargs:
self.server = kwargs['server']
if 'port' in kwargs:
self.port = kwargs['port']
if 'use_ssl' in kwargs:
self.use_ssl = kwargs['use_ssl']
def get_connection_string(self):
protocol = "ssl" if self.use_ssl else "tls"
return f"{protocol}://{self.username}@{self.server}:{self.port}"
# Using default settings
default_client = EmailClient("user1", "pass123")
print(default_client.get_connection_string())
# Output: ssl://user1@smtp.example.com:587
# Overriding defaults
custom_client = EmailClient("user2", "pass456", server="mail.custom.com", port=465)
print(custom_client.get_connection_string())
# Output: ssl://user2@mail.custom.com:465
4. Implementing class methods that work with shared data
class TemperatureConverter:
scales = {
'celsius': {'freezing': 0, 'boiling': 100},
'fahrenheit': {'freezing': 32, 'boiling': 212},
'kelvin': {'freezing': 273.15, 'boiling': 373.15}
}
def __init__(self, temperature, scale):
self.temperature = temperature
self.scale = scale
@classmethod
def add_scale(cls, name, freezing_point, boiling_point):
cls.scales[name] = {'freezing': freezing_point, 'boiling': boiling_point}
def convert_to(self, target_scale):
# First convert to celsius as a common base
source_scale = self.scales[self.scale]
target_scale_values = self.scales[target_scale]
# Calculate percentage between freezing and boiling
range_source = source_scale['boiling'] - source_scale['freezing']
position = (self.temperature - source_scale['freezing']) / range_source
# Apply that percentage to target scale
range_target = target_scale_values['boiling'] - target_scale_values['freezing']
return target_scale_values['freezing'] + position * range_target
# Add a new temperature scale
TemperatureConverter.add_scale('rankine', 0, 671.67)
# Convert between scales
temp = TemperatureConverter(100, 'celsius')
print(temp.convert_to('fahrenheit')) # Output: 212.0
print(temp.convert_to('kelvin')) # Output: 373.15
print(temp.convert_to('rankine')) # Output: 671.67
Use Cases for Instance Variables
Instance variables are the most common type of variables in OOP and are used for:
1. Data unique to each object
class Person:
def __init__(self, name, age, occupation):
self.name = name
self.age = age
self.occupation = occupation
self.friends = []
def add_friend(self, friend_name):
self.friends.append(friend_name)
def describe(self):
friend_text = ", ".join(self.friends) if self.friends else "none"
return f"{self.name} is {self.age} years old, works as a {self.occupation}, and has friends: {friend_text}"
alice = Person("Alice", 28, "Software Engineer")
bob = Person("Bob", 32, "Data Scientist")
alice.add_friend("Charlie")
alice.add_friend("Diana")
bob.add_friend("Eve")
print(alice.describe())
# Output: Alice is 28 years old, works as a Software Engineer, and has friends: Charlie, Diana
print(bob.describe())
# Output: Bob is 32 years old, works as a Data Scientist, and has friends: Eve
2. State that changes throughout an object's lifetime
class BankAccount:
def __init__(self, owner, initial_balance=0):
self.owner = owner
self.balance = initial_balance
self.transaction_history = []
def deposit(self, amount):
if amount <= 0:
return "Deposit amount must be positive"
self.balance += amount
self.transaction_history.append(f"Deposit: +${amount}")
return f"Deposited ${amount}. New balance: ${self.balance}"
def withdraw(self, amount):
if amount <= 0:
return "Withdrawal amount must be positive"
if amount > self.balance:
return "Insufficient funds"
self.balance -= amount
self.transaction_history.append(f"Withdrawal: -${amount}")
return f"Withdrew ${amount}. New balance: ${self.balance}"
def get_statement(self):
statement = f"Account Statement for {self.owner}\n"
statement += f"Current Balance: ${self.balance}\n"
statement += "Transaction History:\n"
for transaction in self.transaction_history:
statement += f"- {transaction}\n"
return statement
account = BankAccount("Alice", 1000)
print(account.deposit(500))
print(account.withdraw(200))
print(account.deposit(50))
print(account.get_statement())
3. Object-specific configuration or settings
class DatabaseConnection:
default_timeout = 30 # Class variable - default for all connections
def __init__(self, host, username, password, **options):
# Required connection parameters
self.host = host
self.username = username
self.password = password
# Optional configuration with defaults
self.port = options.get('port', 3306)
self.database = options.get('database', 'default')
self.timeout = options.get('timeout', self.default_timeout)
self.use_ssl = options.get('use_ssl', True)
self.connection_pool_size = options.get('pool_size', 5)
# State variables
self.is_connected = False
self.connection = None
self.last_query_time = None
def connect(self):
# Simulating connection logic
self.is_connected = True
connection_string = f"{self.username}@{self.host}:{self.port}/{self.database}"
self.connection = f"Simulated connection to {connection_string}"
return f"Connected to {self.host}"
# Creating connections with different configurations
default_conn = DatabaseConnection("db.example.com", "user", "pass123")
custom_conn = DatabaseConnection(
"db.company.com",
"admin",
"secure_pass",
port=5432,
database="customers",
timeout=60,
pool_size=10
)
print(default_conn.timeout) # Output: 30 (using default)
print(custom_conn.timeout) # Output: 60 (custom setting)
print(default_conn.connect()) # Output: Connected to db.example.com
Common Pitfalls and How to Avoid Them
Pitfall 1: Mutable Class Variables
One of the most common pitfalls is using mutable objects as class variables:
class BadShoppingCart:
# Class variable that's a mutable list - DANGER!
items = []
def add_item(self, item):
self.items.append(item)
# Create shopping carts
cart1 = BadShoppingCart()
cart2 = BadShoppingCart()
# Add item to first cart
cart1.add_item("Laptop")
print(cart1.items) # Output: ['Laptop']
# Surprise! The item shows up in cart2 as well
print(cart2.items) # Output: ['Laptop']
# That's because both carts are sharing the same class variable list
The solution is to initialize mutable collections as instance variables in __init__:
class GoodShoppingCart:
def __init__(self):
# Initialize the list as an instance variable
self.items = []
def add_item(self, item):
self.items.append(item)
# Create shopping carts
cart1 = GoodShoppingCart()
cart2 = GoodShoppingCart()
# Add item to first cart
cart1.add_item("Laptop")
print(cart1.items) # Output: ['Laptop']
# Second cart's items are separate
print(cart2.items) # Output: []
Rule of thumb: Never use mutable objects (lists, dictionaries, sets, etc.) as class variables unless you specifically want all instances to share the same collection. If each instance needs its own collection, initialize it in __init__.
Pitfall 2: The Instance Variable Shadow Effect
As demonstrated earlier, modifying a class variable through an instance creates a new instance variable that shadows the class variable. This can lead to confusing behavior:
class Configuration:
debug = False # Class variable
def enable_debug(self):
self.debug = True # Creates instance variable!
# Create configurations
config1 = Configuration()
config2 = Configuration()
# Enable debug on config1
config1.enable_debug()
print(config1.debug) # Output: True
print(config2.debug) # Output: False
# Change the class variable
Configuration.debug = True
print(config1.debug) # Output: True (still using instance variable)
print(config2.debug) # Output: True (using updated class variable)
To avoid this, explicitly access class variables through the class name or self.__class__:
class BetterConfiguration:
debug = False # Class variable
@classmethod
def enable_debug_for_all(cls):
cls.debug = True # Properly modifies class variable
def is_debug_enabled(self):
return self.__class__.debug # Explicitly access class variable
# Create configurations
config1 = BetterConfiguration()
config2 = BetterConfiguration()
# Check initial debug status
print(config1.is_debug_enabled()) # Output: False
print(config2.is_debug_enabled()) # Output: False
# Enable debug for all
BetterConfiguration.enable_debug_for_all()
print(config1.is_debug_enabled()) # Output: True
print(config2.is_debug_enabled()) # Output: True
Pitfall 3: Modifying Class Variables from Instance Methods
When you need to update a class variable from an instance method, always access it through the class name or self.__class__:
class Counter:
count = 0 # Class variable for tracking total count
def __init__(self, name):
self.name = name
self.value = 0 # Instance variable
Counter.count += 1 # Properly increment class variable
def increment(self):
self.value += 1
@classmethod
def get_total_count(cls):
return cls.count
# Create counters
counter1 = Counter("First")
counter2 = Counter("Second")
print(Counter.get_total_count()) # Output: 2
# Adding more counters increments the class variable
counter3 = Counter("Third")
print(Counter.get_total_count()) # Output: 3
Class Variables in Inheritance
Class variables behave in interesting ways in inheritance hierarchies:
class Animal:
species_count = 0
kingdom = "Animalia"
def __init__(self, name):
self.name = name
Animal.species_count += 1
class Dog(Animal):
species = "Canis familiaris"
dog_count = 0
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
Dog.dog_count += 1
def bark(self):
return "Woof!"
class Cat(Animal):
species = "Felis catus"
cat_count = 0
def __init__(self, name, color):
super().__init__(name)
self.color = color
Cat.cat_count += 1
def meow(self):
return "Meow!"
# Create some animals
fido = Dog("Fido", "Golden Retriever")
whiskers = Cat("Whiskers", "Tabby")
rex = Dog("Rex", "German Shepherd")
# Access class variables
print(Animal.species_count) # Output: 3
print(Dog.dog_count) # Output: 2
print(Cat.cat_count) # Output: 1
# Inherited class variables
print(Dog.kingdom) # Output: Animalia
print(fido.kingdom) # Output: Animalia
# Subclass-specific class variables
print(Dog.species) # Output: Canis familiaris
print(Cat.species) # Output: Felis catus
# Modifying inherited class variables
Dog.kingdom = "Modified by Dog"
print(Dog.kingdom) # Output: Modified by Dog
print(Cat.kingdom) # Output: Animalia (unchanged)
print(Animal.kingdom) # Output: Animalia (unchanged)
Key point: Subclasses inherit class variables from their parent classes. If a subclass modifies an inherited class variable, it creates its own version of that variable, which doesn't affect the parent class or other subclasses.
Best Practices
When to Use Class Variables
- For constants that apply to all instances of a class
- For default values that can be overridden by instances
- For tracking information about the class as a whole
- For shared resources that all instances should access
When to Use Instance Variables
- For data unique to each instance
- For state that changes during an object's lifetime
- For values that differ between instances
- For mutable collections that should be separate for each instance
Naming Conventions
- Use ALL_CAPS for class-level constants that will never change
- Use snake_case for regular class variables
- Use snake_case for instance variables
- Prefix private or internal variables with underscore (_)
class FileParser:
# Constants (class variables that won't change)
MAX_FILE_SIZE = 1024 * 1024 * 10 # 10MB
SUPPORTED_FORMATS = ['csv', 'json', 'xml', 'yaml']
# Regular class variables
default_encoding = 'utf-8'
parser_version = '1.0.0'
def __init__(self, file_path, encoding=None):
# Public instance variables
self.file_path = file_path
self.encoding = encoding or self.default_encoding
# Private instance variables
self._parsed_data = None
self._last_parsed = None
def parse(self):
# Simulated parsing logic
extension = self.file_path.split('.')[-1].lower()
if extension not in self.SUPPORTED_FORMATS:
return f"Unsupported format: {extension}"
# ... parsing logic would go here ...
import datetime
self._parsed_data = {"result": f"Parsed {self.file_path} with {self.encoding} encoding"}
self._last_parsed = datetime.datetime.now()
return self._parsed_data
# Using the class with proper variable access
parser = FileParser("data.csv")
print(FileParser.SUPPORTED_FORMATS) # Accessing class constant
result = parser.parse()
print(result)
Using Descriptors and Properties with Class and Instance Variables
For more advanced control over variable access, you can use descriptors and properties:
class PositiveValue:
"""A descriptor that only allows positive values."""
def __init__(self, name):
self.name = name
self.private_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name, 0)
def __set__(self, instance, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self.name} must be a number")
if value < 0:
raise ValueError(f"{self.name} must be positive")
setattr(instance, self.private_name, value)
class Product:
# Class variables
product_count = 0
tax_rate = 0.1
# Descriptors for instance variables
price = PositiveValue("price")
stock = PositiveValue("stock")
def __init__(self, name, price, stock=0):
self.name = name
self.price = price # Uses the descriptor
self.stock = stock # Uses the descriptor
Product.product_count += 1
@property
def total_value(self):
return self.price * self.stock
@property
def price_with_tax(self):
return self.price * (1 + self.tax_rate)
# Create products
laptop = Product("Laptop", 1200, 5)
phone = Product("Smartphone", 800, 10)
print(laptop.price) # Output: 1200
print(laptop.price_with_tax) # Output: 1320.0
print(laptop.total_value) # Output: 6000
# This will raise an error
try:
laptop.price = -100
except ValueError as e:
print(e) # Output: price must be positive
This approach combines class variables, instance variables, descriptors, and properties to create a robust, well-validated class.
Practical Example: Building a Library System
Let's apply our understanding of class and instance variables to build a simple library system:
class LibrarySystem:
# Class variables (shared by all libraries)
MAX_CHECKOUT_DAYS = 14
LATE_FEE_PER_DAY = 0.25
BOOK_CATEGORIES = ["Fiction", "Non-Fiction", "Reference", "Children", "Textbook"]
# To track all library branches
branches = []
@classmethod
def get_all_branches(cls):
return cls.branches
@classmethod
def find_branch_by_name(cls, name):
for branch in cls.branches:
if branch.name == name:
return branch
return None
def __init__(self, name, location):
# Instance variables
self.name = name
self.location = location
self.books = {} # isbn: Book object
self.members = {} # member_id: Member object
self.is_open = False
self._opening_hours = "9 AM - 5 PM"
# Add this branch to the class-level tracking
LibrarySystem.branches.append(self)
def add_book(self, book):
self.books[book.isbn] = book
return f"Added '{book.title}' to {self.name} Library"
def add_member(self, member):
self.members[member.id] = member
return f"Added member {member.name} to {self.name} Library"
def open(self):
self.is_open = True
return f"{self.name} Library is now open"
def close(self):
self.is_open = False
return f"{self.name} Library is now closed"
@property
def opening_hours(self):
return self._opening_hours
@opening_hours.setter
def opening_hours(self, hours):
self._opening_hours = hours
def get_available_books(self):
return [book for book in self.books.values() if book.is_available]
def checkout_book(self, isbn, member_id):
if not self.is_open:
return "Sorry, the library is currently closed"
if isbn not in self.books:
return "Book not found in this library"
if member_id not in self.members:
return "Member not found in this library"
book = self.books[isbn]
member = self.members[member_id]
if not book.is_available:
return f"'{book.title}' is currently unavailable"
if len(member.checked_out_books) >= member.checkout_limit:
return f"{member.name} has reached the checkout limit of {member.checkout_limit} books"
# Process checkout
book.checkout(member_id)
member.add_book(isbn)
return f"'{book.title}' has been checked out to {member.name} for {self.MAX_CHECKOUT_DAYS} days"
class Book:
# Class variables
total_books = 0
def __init__(self, title, author, isbn, category, copies=1):
# Validate category
if category not in LibrarySystem.BOOK_CATEGORIES:
raise ValueError(f"Invalid category. Must be one of: {LibrarySystem.BOOK_CATEGORIES}")
# Instance variables
self.title = title
self.author = author
self.isbn = isbn
self.category = category
self.copies = copies
self.available_copies = copies
self.checkout_history = []
# Increment class counter
Book.total_books += 1
@property
def is_available(self):
return self.available_copies > 0
def checkout(self, member_id):
if self.is_available:
self.available_copies -= 1
import datetime
checkout_date = datetime.datetime.now()
self.checkout_history.append({
"member_id": member_id,
"checkout_date": checkout_date,
"due_date": checkout_date + datetime.timedelta(days=LibrarySystem.MAX_CHECKOUT_DAYS),
"return_date": None
})
return True
return False
def return_book(self, member_id):
# Find the matching checkout record
for record in self.checkout_history:
if record["member_id"] == member_id and record["return_date"] is None:
import datetime
record["return_date"] = datetime.datetime.now()
self.available_copies += 1
# Calculate late fee if applicable
days_overdue = max(0, (record["return_date"] - record["due_date"]).days)
late_fee = days_overdue * LibrarySystem.LATE_FEE_PER_DAY
return {"success": True, "late_fee": late_fee}
return {"success": False, "message": "No matching checkout record found"}
class LibraryMember:
# Class variables for member types and their checkout limits
MEMBER_TYPES = {
"standard": {"checkout_limit": 5, "renewal_limit": 1},
"premium": {"checkout_limit": 10, "renewal_limit": 3},
"student": {"checkout_limit": 7, "renewal_limit": 2}
}
total_members = 0
def __init__(self, name, id, member_type="standard"):
# Validate member type
if member_type not in self.MEMBER_TYPES:
raise ValueError(f"Invalid member type. Must be one of: {list(self.MEMBER_TYPES.keys())}")
# Instance variables
self.name = name
self.id = id
self.member_type = member_type
self.checked_out_books = {} # isbn: checkout_date
self.fine_balance = 0.0
# Get checkout limit from class variable
self.checkout_limit = self.MEMBER_TYPES[member_type]["checkout_limit"]
self.renewal_limit = self.MEMBER_TYPES[member_type]["renewal_limit"]
# Increment member counter
LibraryMember.total_members += 1
def add_book(self, isbn):
import datetime
self.checked_out_books[isbn] = datetime.datetime.now()
def remove_book(self, isbn):
if isbn in self.checked_out_books:
del self.checked_out_books[isbn]
return True
return False
def pay_fine(self, amount):
if amount > self.fine_balance:
return f"Payment amount exceeds fine balance of ${self.fine_balance:.2f}"
self.fine_balance -= amount
return f"Paid ${amount:.2f}. Remaining balance: ${self.fine_balance:.2f}"
def add_fine(self, amount):
self.fine_balance += amount
return f"Added ${amount:.2f} fine. Total balance: ${self.fine_balance:.2f}"
# Using our library system
# Create libraries
main_library = LibrarySystem("Main Branch", "123 Main St")
downtown_library = LibrarySystem("Downtown Branch", "456 Center Ave")
# Create books
book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565", "Fiction", 3)
book2 = Book("To Kill a Mockingbird", "Harper Lee", "9780060935467", "Fiction", 2)
book3 = Book("Python Crash Course", "Eric Matthes", "9781593276034", "Textbook", 1)
# Create members
alice = LibraryMember("Alice Smith", "M001", "premium")
bob = LibraryMember("Bob Johnson", "M002")
charlie = LibraryMember("Charlie Brown", "M003", "student")
# Add books and members to libraries
main_library.add_book(book1)
main_library.add_book(book2)
downtown_library.add_book(book3)
main_library.add_member(alice)
main_library.add_member(bob)
downtown_library.add_member(charlie)
# Open libraries
main_library.open()
downtown_library.open()
# Perform checkouts
print(main_library.checkout_book("9780743273565", "M001"))
print(downtown_library.checkout_book("9781593276034", "M003"))
# Check availability
print(f"Is 'The Great Gatsby' available? {book1.is_available}") # Should show True (2 copies left)
print(f"Is 'Python Crash Course' available? {book3.is_available}") # Should show False (0 copies left)
# Get statistics
print(f"Total books in system: {Book.total_books}")
print(f"Total members in system: {LibraryMember.total_members}")
print(f"Library branches: {[branch.name for branch in LibrarySystem.get_all_branches()]}")
# Find a specific branch
branch = LibrarySystem.find_branch_by_name("Downtown Branch")
if branch:
print(f"Found branch: {branch.name} at {branch.location}")
In this comprehensive example:
- Class variables are used for system-wide constants, settings, and tracking of all instances
- Class methods operate on those class variables to provide system-wide functionality
- Instance variables store the unique state of each library, book, and member
- Properties provide controlled access to derived values like
is_available - The system properly manages the relationships between objects using both class and instance variables
Conclusion
Understanding the difference between instance and class variables is crucial for effective Python programming. Let's summarize what we've learned:
Key Differences
| Feature | Class Variables | Instance Variables |
|---|---|---|
| Definition Location | In the class body, outside methods | Usually in __init__ with self prefix |
| Scope | Shared by all instances | Unique to each instance |
| Access Through | Class name or instance | Instance only |
| Memory Usage | One copy for the class | One copy per instance |
| Best For | Constants, shared data, counters | Object state, unique attributes |
Best Practices
- Use class variables for data that should be shared across all instances
- Use instance variables for data that should be unique to each instance
- Never use mutable objects as class variables unless sharing is intended
- Access class variables through the class name when modifying them
- Be aware of the "shadow effect" when modifying class variables through instances
- Use descriptors or properties for controlled access to variables
With this knowledge, you can design more effective and efficient classes in Python, avoiding common pitfalls while leveraging the flexibility that Python's variable system provides.
Practice Exercise
Design a University class system with the following requirements:
- The
Universityclass should track all departments and students using class variables - The
Departmentclass should have department-specific constants and track all courses - The
Courseclass should track enrollment and have a maximum capacity - The
Studentclass should track enrolled courses and calculate GPA
Make sure to use class and instance variables appropriately, and include methods to manage the relationships between these classes.
Additional Resources
- Python Documentation: Class and Instance Variables
- Real Python: Class and Instance Attributes
- Real Python: Instance, Class, and Static Methods Demystified
- Python Documentation: Descriptors
- Recommended Book: "Fluent Python" by Luciano Ramalho (Chapter 9: A Pythonic Object)