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.

Python Reading Strategy: Pay attention to Python’s readable syntax, built-in functions, and how the language promotes clear, concise code. Notice the use of conventions like underscore prefixes and descriptive variable names.

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

  • self parameter represents the instance being created
  • Default parameters: initial_balance=0.0 provides 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.balance

Data protection patterns:

  • _balance is 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}"

  • :.2f formats 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 input
List 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 None checks 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 conditions

Truthiness 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.0

Practice 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.

← Previous: Beginner Basics Next: Control Structures →