Python Fundamentals: Classes, Functions, and Data Structures
This section focuses on reading Python code that demonstrates object-oriented programming, function design, and Python’s powerful built-in data structures.
How to Read Python Code
- Start with function/class purpose – Read docstrings and comments first.
- Look for Python idioms – List comprehensions, built-in functions, context managers.
- Track data transformations – Observe how data moves through functions and methods.
- Notice Python conventions – Pay attention to naming, structure, and style patterns.
Example 1: Object-Oriented Design with Properties and Methods
Code to Read:
 1class BankAccount:
 2    def __init__(self, account_holder, account_number, initial_balance=0.0):
 3        self.account_holder = account_holder
 4        self.account_number = account_number
 5        self._balance = initial_balance  # Protected attribute
 6        self._transaction_history = []
 7    
 8    def deposit(self, amount):
 9        if amount <= 0:
10            print("Invalid deposit amount")
11            return False
12        
13        self._balance += amount
14        self._transaction_history.append(f"Deposited ${amount:.2f}")
15        print(f"Deposited ${amount:.2f}. New balance: ${self._balance:.2f}")
16        return True
17    
18    def withdraw(self, amount):
19        if amount <= 0:
20            print("Invalid withdrawal amount")
21            return False
22        
23        if amount > self._balance:
24            print("Insufficient funds")
25            return False
26        
27        self._balance -= amount
28        self._transaction_history.append(f"Withdrew ${amount:.2f}")
29        print(f"Withdrew ${amount:.2f}. Remaining balance: ${self._balance:.2f}")
30        return True
31    
32    @property
33    def balance(self):
34        """Read-only access to balance"""
35        return self._balance
36    
37    def get_transaction_history(self):
38        return self._transaction_history.copy()  # Return a copy for safety
39    
40    def __str__(self):
41        return f"Account({self.account_holder}, #{self.account_number}, ${self._balance:.2f})"
42    
43    def __repr__(self):
44        return f"BankAccount('{self.account_holder}', {self.account_number}, {self._balance})"
45
46# Usage example
47if __name__ == "__main__":
48    account = BankAccount("Alice Johnson", 12345, 100.0)
49    account.deposit(50.0)
50    account.withdraw(25.0)
51    print(account)
52    print("History:", account.get_transaction_history())What to Notice:
Python Class Structure and Conventions
Constructor (__init__): Python’s object initialization method
- selfparameter represents the instance being created
- Default parameters: initial_balance=0.0provides sensible defaults
Attribute naming conventions:
- self.account_holder- public attribute
- self._balance- protected attribute (single underscore convention)
- self._transaction_history- internal data structure
Magic methods:
- __str__()- user-friendly string representation
- __repr__()- developer/debugging string representation
Property Decorators and Encapsulation
@property decorator: Makes balance accessible like an attribute
1# Instead of: account.get_balance()
2# You can write: account.balanceData protection patterns:
- _balanceis protected (convention, not enforced)
- get_transaction_history()returns a copy to prevent external modification
- Methods validate input before modifying state
String Formatting and F-strings
F-string formatting: f"Deposited ${amount:.2f}"
- :.2fformats floating point to 2 decimal places
- More readable than older %or.format()methods
Early returns for validation:
1if amount <= 0:
2    print("Invalid deposit amount")
3    return False  # Exit early on invalid inputList Operations and Data Safety
List methods:
- append()adds items to transaction history
- copy()creates shallow copy for data protection
Main guard: if __name__ == "__main__":
- Runs code only when script is executed directly
- Prevents execution when module is imported
Trace Through Example:
 1# Object creation:
 2account = BankAccount("Alice Johnson", 12345, 100.0)
 3# → account_holder="Alice Johnson", account_number=12345, 
 4#   _balance=100.0, _transaction_history=[]
 5
 6# Deposit operation:
 7account.deposit(50.0)
 8# → amount > 0: True, _balance = 100.0 + 50.0 = 150.0
 9# → _transaction_history = ["Deposited $50.00"]
10# → returns True
11
12# Withdraw operation:
13account.withdraw(25.0)
14# → amount > 0: True, amount <= _balance: True
15# → _balance = 150.0 - 25.0 = 125.0
16# → _transaction_history = ["Deposited $50.00", "Withdrew $25.00"]
17# → returns True
18
19# Property access:
20print(account.balance)  # → 125.0 (calls the @property method)Example 2: Functional Programming with List Processing
Code to Read:
 1def analyze_student_grades(grades):
 2    """Analyze a list of student grades and return statistics."""
 3    if not grades:  # Pythonic way to check for empty list
 4        return {
 5            'error': 'No grades to analyze',
 6            'count': 0
 7        }
 8    
 9    # Filter out invalid grades (None, negative, or > 100)
10    valid_grades = [grade for grade in grades 
11                   if grade is not None and 0 <= grade <= 100]
12    
13    if not valid_grades:
14        return {'error': 'No valid grades found', 'count': 0}
15    
16    # Calculate basic statistics using built-in functions
17    total_students = len(valid_grades)
18    average = sum(valid_grades) / total_students
19    highest = max(valid_grades)
20    lowest = min(valid_grades)
21    
22    # Nested function for letter grade calculation
23    def get_letter_grade(score):
24        if score >= 90: return 'A'
25        elif score >= 80: return 'B'
26        elif score >= 70: return 'C'
27        elif score >= 60: return 'D'
28        else: return 'F'
29    
30    # Use Counter for frequency counting
31    from collections import Counter
32    letter_grades = [get_letter_grade(grade) for grade in valid_grades]
33    grade_distribution = Counter(letter_grades)
34    
35    # Calculate passing rate (>= 60)
36    passing_grades = [grade for grade in valid_grades if grade >= 60]
37    passing_rate = len(passing_grades) / total_students * 100
38    
39    return {
40        'count': total_students,
41        'average': round(average, 2),
42        'highest': highest,
43        'lowest': lowest,
44        'passing_rate': round(passing_rate, 1),
45        'grade_distribution': dict(grade_distribution),
46        'valid_grades': valid_grades
47    }
48
49def display_grade_analysis(analysis):
50    """Display the grade analysis in a formatted way."""
51    if 'error' in analysis:
52        print(f"Error: {analysis['error']}")
53        return
54    
55    print("=== Grade Analysis ===")
56    print(f"Total students: {analysis['count']}")
57    print(f"Average grade: {analysis['average']}")
58    print(f"Highest grade: {analysis['highest']}")
59    print(f"Lowest grade: {analysis['lowest']}")
60    print(f"Passing rate: {analysis['passing_rate']}%")
61    
62    print("\nGrade Distribution:")
63    for letter, count in sorted(analysis['grade_distribution'].items()):
64        percentage = (count / analysis['count']) * 100
65        print(f"  {letter}: {count} students ({percentage:.1f}%)")
66
67# Example usage
68if __name__ == "__main__":
69    # Test with various scenarios
70    test_grades = [85, 92, 78, 67, 94, 81, 88, 76, 90, 82]
71    analysis = analyze_student_grades(test_grades)
72    display_grade_analysis(analysis)
73    
74    # Test with invalid data
75    print("\n" + "="*40)
76    invalid_grades = [85, None, -5, 110, 78]
77    analysis2 = analyze_student_grades(invalid_grades)
78    display_grade_analysis(analysis2)What to Notice:
List Comprehensions and Filtering
List comprehension with filtering:
1valid_grades = [grade for grade in grades 
2               if grade is not None and 0 <= grade <= 100]- Combines iteration, filtering, and list creation in one line
- More concise than equivalent for loop with if statements
- grade is not Nonechecks for None values specifically
Multiple filtering patterns:
1passing_grades = [grade for grade in valid_grades if grade >= 60]
2letter_grades = [get_letter_grade(grade) for grade in valid_grades]Built-in Functions and Data Processing
Statistical functions:
- len()- count items
- sum()- add all numbers
- max()/- min()- find extremes
- round()- control decimal precision
Collections module:
- Counter()automatically counts frequency of items
- More efficient than manual counting loops
Function Design Patterns
Nested function: get_letter_grade() defined inside analyze_student_grades()
- Only accessible within the parent function
- Encapsulates helper logic
- Has access to parent function’s variables
Return dictionary pattern:
- Functions return structured data as dictionaries
- Makes it easy to access specific results
- Self-documenting with key names
Error Handling and Edge Cases
Defensive programming:
1if not grades:  # Handle empty input
2if not valid_grades:  # Handle no valid data
3if 'error' in analysis:  # Handle error conditionsTruthiness in Python:
- if not grades:- empty lists are “falsy”
- More Pythonic than if len(grades) == 0:
Trace Through Example:
 1# Input: [85, 92, 78, 67, 94]
 2grades = [85, 92, 78, 67, 94]
 3
 4# Filtering step:
 5valid_grades = [85, 92, 78, 67, 94]  # All valid (0-100, not None)
 6
 7# Statistics:
 8total_students = 5
 9average = (85 + 92 + 78 + 67 + 94) / 5 = 83.2
10highest = 94, lowest = 67
11
12# Letter grades:
13[get_letter_grade(85), get_letter_grade(92), ...] 
14= ['B', 'A', 'C', 'D', 'A']
15
16# Counter result:
17grade_distribution = {'A': 2, 'B': 1, 'C': 1, 'D': 1}
18
19# Passing rate:
20passing_grades = [85, 92, 78, 67, 94]  # All >= 60
21passing_rate = 5/5 * 100 = 100.0Practice Exercises
Reading Exercise 1: Trace through the BankAccount class with these operations:
1account = BankAccount("Bob", 54321, 200.0)
2account.withdraw(250.0)  # What happens?
3account.deposit(-50.0)   # What happens?Reading Exercise 2: Predict the output of analyze_student_grades([95, None, 105, 45, 88]). What gets filtered out and why?
Reading Exercise 3: Identify all the places where Python’s “Pythonic” style makes the code more readable than equivalent Java code would be.