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
self
parameter represents the instance being created- Default parameters:
initial_balance=0.0
provides sensible defaults
Attribute naming conventions:
self.account_holder
- public attributeself._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 historycopy()
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 itemssum()
- add all numbersmax()
/min()
- find extremesround()
- 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.