Python Programming Crash Course

Master Python from Basics to Advanced Concepts

Lesson 1: Python Basics

Introduction to Python

Python is a high-level, interpreted programming language known for its simplicity and readability. It's widely used in web development, data science, artificial intelligence, automation, and more.

Why Python? Python's clean syntax makes it perfect for beginners, while its powerful libraries make it indispensable for professionals.

Variables and Data Types

Variables in Python are dynamically typed, meaning you don't need to declare their type explicitly.

# Variables - no type declaration needed name = "Alice" # String age = 25 # Integer height = 5.6 # Float is_student = True # Boolean # Multiple assignment x, y, z = 1, 2, 3 # Print variables print(name, age, height, is_student)

Basic Data Types

# Strings greeting = "Hello, World!" multiline = """This is a multiline string""" # Numbers integer = 42 floating = 3.14 complex_num = 2 + 3j # Boolean is_true = True is_false = False # Type checking print(type(greeting)) # <class 'str'> print(type(integer)) # <class 'int'>

Basic Operators

# Arithmetic operators addition = 10 + 5 # 15 subtraction = 10 - 5 # 5 multiplication = 10 * 5 # 50 division = 10 / 5 # 2.0 (always float) floor_division = 10 // 3 # 3 modulus = 10 % 3 # 1 exponent = 2 ** 3 # 8 # Comparison operators print(5 == 5) # True print(5 != 3) # True print(5 > 3) # True print(5 < 3) # False # Logical operators print(True and False) # False print(True or False) # True print(not True) # False

String Operations

# String manipulation first_name = "John" last_name = "Doe" # Concatenation full_name = first_name + " " + last_name # String methods upper_case = full_name.upper() # "JOHN DOE" lower_case = full_name.lower() # "john doe" length = len(full_name) # 8 # String formatting (f-strings) age = 30 message = f"{first_name} is {age} years old" print(message) # John is 30 years old # String indexing and slicing text = "Python" print(text[0]) # 'P' print(text[-1]) # 'n' print(text[0:3]) # 'Pyt'

User Input and Output

# Getting user input # name = input("Enter your name: ") # age = int(input("Enter your age: ")) # Formatted output name = "Alice" score = 95.5 print(f"Student: {name}, Score: {score:.1f}") # Multiple print arguments print("Hello", "World", sep="-", end="!\n") # Output: Hello-World!
Common Pitfall: The input() function always returns a string. Convert it to int or float if you need numeric values: age = int(input("Age: "))

Basic Syntax Rules

  • Python uses indentation (not braces) to define code blocks
  • Variable names are case-sensitive (age and Age are different)
  • Use # for single-line comments and """...""" for multi-line comments
  • Statements end with a newline (no semicolons needed)
  • Use snake_case for variable names: my_variable
Best Practice: Use descriptive variable names like student_count instead of sc. Your code should be self-documenting.

Test Your Knowledge - Lesson 1

1. What will be the output of: print(type(5.0))?

2. Which operator is used for floor division in Python?

3. What is the correct way to create an f-string in Python?

Lesson 2: Control Flow

If/Else Statements

Conditional statements allow your code to make decisions based on conditions.

# Simple if statement age = 18 if age >= 18: print("You are an adult") # If-else statement temperature = 25 if temperature > 30: print("It's hot!") else: print("It's comfortable") # If-elif-else statement score = 85 if score >= 90: grade = "A" elif score >= 80: grade = "B" elif score >= 70: grade = "C" elif score >= 60: grade = "D" else: grade = "F" print(f"Your grade is: {grade}")
Note: Python uses indentation (usually 4 spaces) to define code blocks. All statements in the same block must have the same indentation level.

Comparison and Logical Operators

# Comparison operators x = 10 y = 20 print(x == y) # Equal to print(x != y) # Not equal to print(x > y) # Greater than print(x < y) # Less than print(x >= y) # Greater than or equal to print(x <= y) # Less than or equal to # Logical operators - combining conditions age = 25 has_license = True if age >= 18 and has_license: print("You can drive") # Or operator is_weekend = True is_holiday = False if is_weekend or is_holiday: print("No work today!") # Not operator is_raining = False if not is_raining: print("Let's go outside!")

For Loops

For loops iterate over sequences (lists, strings, ranges, etc.).

# Looping through a range for i in range(5): print(i) # Prints 0, 1, 2, 3, 4 # Range with start and end for i in range(1, 6): print(i) # Prints 1, 2, 3, 4, 5 # Range with step for i in range(0, 10, 2): print(i) # Prints 0, 2, 4, 6, 8 # Looping through a string for char in "Python": print(char) # Looping through a list fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit) # Enumerate - getting index and value for index, fruit in enumerate(fruits): print(f"{index}: {fruit}")

While Loops

While loops continue executing as long as a condition is true.

# Basic while loop count = 0 while count < 5: print(f"Count: {count}") count += 1 # While with condition password = "" # In real code: while password != "secret": # password = input("Enter password: ") # print("Access granted!") # While with counter number = 1 total = 0 while number <= 10: total += number number += 1 print(f"Sum of 1 to 10: {total}")
Warning: Be careful with while loops! Make sure the condition eventually becomes false, or you'll create an infinite loop.

Break and Continue

# Break - exit the loop early for i in range(10): if i == 5: break print(i) # Prints 0, 1, 2, 3, 4 # Continue - skip to next iteration for i in range(5): if i == 2: continue print(i) # Prints 0, 1, 3, 4 # Break with while loop count = 0 while True: print(count) count += 1 if count >= 5: break # Practical example - finding first even number numbers = [1, 3, 5, 8, 9, 10] for num in numbers: if num % 2 == 0: print(f"First even number: {num}") break

Functions

Functions are reusable blocks of code that perform specific tasks.

# Basic function def greet(): print("Hello, World!") greet() # Call the function # Function with parameters def greet_person(name): print(f"Hello, {name}!") greet_person("Alice") # Function with return value def add(a, b): return a + b result = add(5, 3) print(result) # 8 # Function with default parameters def greet_with_title(name, title="Mr."): return f"Hello, {title} {name}" print(greet_with_title("Smith")) # Hello, Mr. Smith print(greet_with_title("Jones", "Dr.")) # Hello, Dr. Jones # Multiple return values def get_min_max(numbers): return min(numbers), max(numbers) minimum, maximum = get_min_max([1, 5, 3, 9, 2]) print(f"Min: {minimum}, Max: {maximum}")
Best Practice: Functions should do one thing and do it well. Use descriptive names that explain what the function does: calculate_total() instead of calc().

Function Scope

# Local vs Global variables global_var = "I'm global" def my_function(): local_var = "I'm local" print(global_var) # Can access global print(local_var) # Can access local my_function() # print(local_var) # Error! local_var doesn't exist here # Modifying global variables counter = 0 def increment(): global counter counter += 1 increment() print(counter) # 1

Test Your Knowledge - Lesson 2

1. What will this code print?
for i in range(2, 8, 2):
    print(i)

2. Which keyword is used to exit a loop prematurely?

3. What is the correct syntax to define a function that takes two parameters and returns their sum?

Lesson 3: Data Structures

Lists

Lists are ordered, mutable collections that can contain items of different types.

# Creating lists fruits = ["apple", "banana", "cherry"] numbers = [1, 2, 3, 4, 5] mixed = [1, "hello", 3.14, True] empty = [] # Accessing elements print(fruits[0]) # 'apple' print(fruits[-1]) # 'cherry' (last element) print(fruits[0:2]) # ['apple', 'banana'] (slicing) # Modifying lists fruits[1] = "blueberry" print(fruits) # ['apple', 'blueberry', 'cherry'] # Adding elements fruits.append("date") # Add to end fruits.insert(1, "apricot") # Insert at index fruits.extend(["fig", "grape"]) # Add multiple # Removing elements fruits.remove("apple") # Remove by value popped = fruits.pop() # Remove and return last del fruits[0] # Delete by index # List methods numbers = [3, 1, 4, 1, 5, 9, 2, 6] numbers.sort() # Sort in place print(numbers.count(1)) # Count occurrences print(numbers.index(4)) # Find index of value numbers.reverse() # Reverse in place
Tip: Lists are mutable, meaning you can change their contents after creation. Use list.copy() to create a separate copy instead of a reference.

List Comprehensions

A concise way to create lists based on existing lists or ranges.

# Basic list comprehension squares = [x**2 for x in range(10)] print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # With condition evens = [x for x in range(20) if x % 2 == 0] print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # Transform strings fruits = ["apple", "banana", "cherry"] upper_fruits = [fruit.upper() for fruit in fruits] print(upper_fruits) # ['APPLE', 'BANANA', 'CHERRY'] # Nested list comprehension matrix = [[i*j for j in range(1, 4)] for i in range(1, 4)] # [[1, 2, 3], [2, 4, 6], [3, 6, 9]] # If-else in comprehension nums = [1, 2, 3, 4, 5] labels = ["even" if x % 2 == 0 else "odd" for x in nums]

Tuples

Tuples are ordered, immutable collections. Once created, they cannot be changed.

# Creating tuples coordinates = (10, 20) person = ("Alice", 25, "Engineer") single = (42,) # Note the comma for single-element tuple empty = () # Accessing elements print(coordinates[0]) # 10 print(person[1:3]) # (25, 'Engineer') # Tuple unpacking x, y = coordinates name, age, job = person # Tuples are immutable # coordinates[0] = 15 # This would cause an error! # Multiple return values (actually returns a tuple) def get_user(): return "Bob", 30, "Designer" name, age, job = get_user() # Named tuples for clarity from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) p = Point(10, 20) print(p.x, p.y)
Common Mistake: To create a single-element tuple, you must include a trailing comma: (42,). Without it, (42) is just the number 42 in parentheses.

Dictionaries

Dictionaries store key-value pairs and provide fast lookup by key.

# Creating dictionaries student = { "name": "Alice", "age": 20, "major": "Computer Science" } # Accessing values print(student["name"]) # 'Alice' print(student.get("age")) # 20 print(student.get("grade", "N/A")) # Default value # Modifying dictionaries student["age"] = 21 # Update value student["gpa"] = 3.8 # Add new key-value del student["major"] # Delete key-value # Dictionary methods print(student.keys()) # Get all keys print(student.values()) # Get all values print(student.items()) # Get key-value pairs # Looping through dictionaries for key, value in student.items(): print(f"{key}: {value}") # Dictionary comprehension squares = {x: x**2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} # Nested dictionaries students = { "student1": {"name": "Alice", "age": 20}, "student2": {"name": "Bob", "age": 22} }

Sets

Sets are unordered collections of unique elements.

# Creating sets fruits = {"apple", "banana", "cherry"} numbers = {1, 2, 3, 4, 5} empty_set = set() # Note: {} creates an empty dict, not set # Adding and removing fruits.add("date") fruits.remove("banana") # Raises error if not found fruits.discard("grape") # No error if not found # Set operations a = {1, 2, 3, 4} b = {3, 4, 5, 6} print(a | b) # Union: {1, 2, 3, 4, 5, 6} print(a & b) # Intersection: {3, 4} print(a - b) # Difference: {1, 2} print(a ^ b) # Symmetric difference: {1, 2, 5, 6} # Removing duplicates numbers = [1, 2, 2, 3, 3, 3, 4] unique = list(set(numbers)) # [1, 2, 3, 4] # Set comprehension even_squares = {x**2 for x in range(10) if x % 2 == 0}
Use Case: Sets are perfect for removing duplicates and testing membership. Checking if an element is in a set is much faster than checking a list.

Choosing the Right Data Structure

  • List: When you need an ordered, mutable collection that allows duplicates
  • Tuple: When you need an ordered, immutable collection (faster than lists)
  • Dictionary: When you need key-value pairs for fast lookup
  • Set: When you need unique elements and set operations

Test Your Knowledge - Lesson 3

1. Which data structure is immutable?

2. What will [x*2 for x in range(3)] produce?

3. How do you access a value in a dictionary named 'data' with key 'name'?

Lesson 4: Object-Oriented Programming

Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm that organizes code into objects that contain both data (attributes) and behavior (methods).

Key Concepts: Classes are blueprints for creating objects. Objects are instances of classes that contain actual data.

Classes and Objects

# Defining a class class Dog: # Class attribute (shared by all instances) species = "Canis familiaris" # Constructor (initializer) def __init__(self, name, age): # Instance attributes (unique to each instance) self.name = name self.age = age # Instance method def bark(self): return f"{self.name} says Woof!" def get_info(self): return f"{self.name} is {self.age} years old" # Creating objects (instances) dog1 = Dog("Buddy", 3) dog2 = Dog("Lucy", 5) # Accessing attributes and methods print(dog1.name) # Buddy print(dog1.bark()) # Buddy says Woof! print(dog2.get_info()) # Lucy is 5 years old print(Dog.species) # Canis familiaris

The __init__ Method and self

The __init__ method is called when creating a new instance. The self parameter refers to the instance being created.

# More complex class example class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance self.transactions = [] def deposit(self, amount): if amount > 0: self.balance += amount self.transactions.append(f"Deposit: +${amount}") return f"Deposited ${amount}. New balance: ${self.balance}" return "Invalid amount" def withdraw(self, amount): if amount > self.balance: return "Insufficient funds" if amount > 0: self.balance -= amount self.transactions.append(f"Withdrawal: -${amount}") return f"Withdrew ${amount}. New balance: ${self.balance}" return "Invalid amount" def get_balance(self): return f"Current balance: ${self.balance}" # Using the class account = BankAccount("Alice", 1000) print(account.deposit(500)) # Deposited $500. New balance: $1500 print(account.withdraw(200)) # Withdrew $200. New balance: $1300 print(account.get_balance()) # Current balance: $1300
Important: Always include self as the first parameter in instance methods. Python passes the instance automatically when you call the method.

Inheritance

Inheritance allows a class to inherit attributes and methods from another class.

# Parent class (base class) class Animal: def __init__(self, name): self.name = name def speak(self): return "Some sound" def info(self): return f"I am {self.name}" # Child class (derived class) class Dog(Animal): def __init__(self, name, breed): super().__init__(name) # Call parent constructor self.breed = breed def speak(self): # Override parent method return "Woof!" def fetch(self): # New method specific to Dog return f"{self.name} is fetching the ball" class Cat(Animal): def speak(self): return "Meow!" # Using inheritance dog = Dog("Buddy", "Golden Retriever") cat = Cat("Whiskers") print(dog.info()) # I am Buddy (inherited) print(dog.speak()) # Woof! (overridden) print(dog.fetch()) # Buddy is fetching the ball print(cat.speak()) # Meow! (overridden)

Encapsulation

Encapsulation restricts direct access to some of an object's components.

# Public, protected, and private attributes class Person: def __init__(self, name, age, ssn): self.name = name # Public self._age = age # Protected (by convention) self.__ssn = ssn # Private (name mangling) # Getter method def get_age(self): return self._age # Setter method def set_age(self, age): if age > 0: self._age = age else: raise ValueError("Age must be positive") # Private method def __verify_ssn(self): return len(self.__ssn) == 9 # Public method using private method def validate(self): return self.__verify_ssn() person = Person("Alice", 30, "123456789") print(person.name) # OK - public print(person.get_age()) # OK - using getter # print(person.__ssn) # Error - private attribute # Property decorator (Pythonic way) class Employee: def __init__(self, name, salary): self._name = name self._salary = salary @property def salary(self): return self._salary @salary.setter def salary(self, value): if value < 0: raise ValueError("Salary cannot be negative") self._salary = value emp = Employee("Bob", 50000) print(emp.salary) # Uses getter emp.salary = 55000 # Uses setter

Special Methods (Magic Methods)

# Special methods customize class behavior class Book: def __init__(self, title, author, pages): self.title = title self.author = author self.pages = pages # String representation for users def __str__(self): return f"'{self.title}' by {self.author}" # String representation for developers def __repr__(self): return f"Book('{self.title}', '{self.author}', {self.pages})" # Length def __len__(self): return self.pages # Comparison def __eq__(self, other): return self.pages == other.pages def __lt__(self, other): return self.pages < other.pages book1 = Book("1984", "George Orwell", 328) book2 = Book("Brave New World", "Aldous Huxley", 268) print(str(book1)) # '1984' by George Orwell print(len(book1)) # 328 print(book1 == book2) # False print(book2 < book1) # True (fewer pages)
Best Practice: Use OOP when you have related data and behavior that should be grouped together. It makes code more organized, reusable, and easier to maintain.

Class Methods and Static Methods

# Class methods and static methods class MathOperations: pi = 3.14159 def __init__(self, value): self.value = value # Instance method (uses self) def square(self): return self.value ** 2 # Class method (uses cls) @classmethod def circle_area(cls, radius): return cls.pi * radius ** 2 # Static method (uses neither self nor cls) @staticmethod def add(a, b): return a + b # Usage obj = MathOperations(5) print(obj.square()) # 25 print(MathOperations.circle_area(10)) # 314.159 print(MathOperations.add(3, 4)) # 7

Test Your Knowledge - Lesson 4

1. What is the purpose of the __init__ method in a Python class?

2. Which keyword is used to inherit from a parent class?

3. What does a single underscore prefix (e.g., _variable) indicate in Python?

Lesson 5: Modules and Packages

What are Modules?

Modules are Python files containing functions, classes, and variables that can be imported and used in other Python programs.

Benefits: Modules help organize code, promote reusability, and maintain namespace separation.

Importing Modules

# Import entire module import math print(math.pi) # 3.141592653589793 print(math.sqrt(16)) # 4.0 # Import specific items from math import pi, sqrt print(pi) # 3.141592653589793 print(sqrt(25)) # 5.0 # Import with alias import math as m print(m.ceil(4.2)) # 5 # Import all (not recommended) from math import * print(floor(4.8)) # 4 # Import from submodule from datetime import datetime now = datetime.now() print(now)
Warning: Avoid using from module import * as it pollutes the namespace and makes code harder to understand. Always prefer explicit imports.

Standard Library Modules

Python comes with a rich standard library. Here are some essential modules:

# os - Operating system interface import os current_dir = os.getcwd() # os.mkdir("new_folder") # os.listdir(".") # datetime - Date and time operations from datetime import datetime, timedelta now = datetime.now() tomorrow = now + timedelta(days=1) formatted = now.strftime("%Y-%m-%d %H:%M:%S") # random - Random number generation import random random_num = random.randint(1, 100) random_choice = random.choice(['a', 'b', 'c']) random.shuffle([1, 2, 3, 4, 5]) # json - JSON encoding/decoding import json data = {"name": "Alice", "age": 30} json_string = json.dumps(data) parsed = json.loads(json_string) # collections - Specialized container types from collections import Counter, defaultdict counts = Counter(['a', 'b', 'a', 'c', 'b', 'a']) print(counts) # Counter({'a': 3, 'b': 2, 'c': 1}) # sys - System-specific parameters import sys print(sys.version) # Python version # print(sys.argv) # Command-line arguments

Creating Your Own Modules

Any Python file can be imported as a module.

# File: mymath.py """ A simple math utilities module. """ def add(a, b): """Add two numbers.""" return a + b def multiply(a, b): """Multiply two numbers.""" return a * b PI = 3.14159 # This runs only when file is executed directly if __name__ == "__main__": print("Testing module...") print(add(2, 3)) # File: main.py # import mymath # result = mymath.add(5, 3) # print(mymath.PI)
Best Practice: Use if __name__ == "__main__": to include code that should only run when the file is executed directly, not when imported.

Packages

Packages are directories containing multiple modules and a special __init__.py file.

# Package structure: # mypackage/ # __init__.py # module1.py # module2.py # subpackage/ # __init__.py # module3.py # Using packages # from mypackage import module1 # from mypackage.subpackage import module3 # import mypackage.module2 as m2

pip - Python Package Manager

# Installing packages (run in terminal, not Python) # pip install requests # pip install numpy pandas matplotlib # pip install package==1.2.3 # Specific version # Listing installed packages # pip list # Uninstalling packages # pip uninstall requests # Creating requirements file # pip freeze > requirements.txt # Installing from requirements # pip install -r requirements.txt # Upgrading packages # pip install --upgrade package_name

Virtual Environments

Virtual environments create isolated Python environments for different projects.

# Creating virtual environment (terminal) # python -m venv myenv # Activating (Windows) # myenv\Scripts\activate # Activating (Mac/Linux) # source myenv/bin/activate # Deactivating # deactivate # Why use virtual environments? # - Isolate project dependencies # - Avoid version conflicts # - Easy to recreate environment # - Keep global Python clean
Best Practice: Always use virtual environments for your projects. It prevents dependency conflicts and makes your project reproducible.

File I/O Operations

# Reading files # Method 1: Manual close file = open("example.txt", "r") content = file.read() file.close() # Method 2: With context manager (recommended) with open("example.txt", "r") as file: content = file.read() # File automatically closed after with block # Reading line by line with open("example.txt", "r") as file: for line in file: print(line.strip()) # Reading all lines into a list with open("example.txt", "r") as file: lines = file.readlines() # Writing files with open("output.txt", "w") as file: file.write("Hello, World!\n") file.write("Second line\n") # Appending to file with open("output.txt", "a") as file: file.write("Appended line\n") # Reading and writing modes: # "r" - Read (default) # "w" - Write (overwrites) # "a" - Append # "r+" - Read and write # "b" - Binary mode (e.g., "rb", "wb")

Working with CSV and JSON

# CSV files import csv # Writing CSV data = [ ["Name", "Age", "City"], ["Alice", 30, "New York"], ["Bob", 25, "Los Angeles"] ] with open("data.csv", "w", newline="") as file: writer = csv.writer(file) writer.writerows(data) # Reading CSV with open("data.csv", "r") as file: reader = csv.reader(file) for row in reader: print(row) # CSV DictReader/DictWriter with open("data.csv", "r") as file: reader = csv.DictReader(file) for row in reader: print(row["Name"], row["Age"]) # JSON files import json # Writing JSON data = { "name": "Alice", "age": 30, "skills": ["Python", "JavaScript"] } with open("data.json", "w") as file: json.dump(data, file, indent=2) # Reading JSON with open("data.json", "r") as file: loaded_data = json.load(file)
Common Error: Always use newline="" when opening CSV files in write mode on Windows to avoid extra blank lines.

Test Your Knowledge - Lesson 5

1. What is the recommended way to open and read a file in Python?

2. What command is used to install a Python package?

3. What does the __name__ == "__main__" check do?

Lesson 6: Advanced Python

Error Handling

Error handling allows your program to gracefully handle unexpected situations.

# Basic try-except try: number = int(input("Enter a number: ")) result = 10 / number print(f"Result: {result}") except ValueError: print("Invalid input! Please enter a number.") except ZeroDivisionError: print("Cannot divide by zero!") # Multiple exceptions try: # Some code pass except (ValueError, TypeError) as e: print(f"Error occurred: {e}") # Generic exception handler try: # Some code pass except Exception as e: print(f"An error occurred: {e}") # Try-except-else-finally try: file = open("data.txt", "r") data = file.read() except FileNotFoundError: print("File not found!") else: # Runs if no exception occurred print("File read successfully") finally: # Always runs, even if exception occurs print("Cleanup completed")
Best Practice: Catch specific exceptions instead of using a bare except:. This makes debugging easier and prevents hiding unexpected errors.

Raising Exceptions

# Raising exceptions def validate_age(age): if age < 0: raise ValueError("Age cannot be negative") if age > 150: raise ValueError("Age seems unrealistic") return True try: validate_age(-5) except ValueError as e: print(f"Validation error: {e}") # Custom exceptions class InsufficientFundsError(Exception): """Raised when account has insufficient funds""" pass class BankAccount: def __init__(self, balance): self.balance = balance def withdraw(self, amount): if amount > self.balance: raise InsufficientFundsError( f"Cannot withdraw ${amount}. Balance: ${self.balance}" ) self.balance -= amount account = BankAccount(100) try: account.withdraw(150) except InsufficientFundsError as e: print(e)

Decorators

Decorators are functions that modify the behavior of other functions.

# Simple decorator def my_decorator(func): def wrapper(): print("Before function call") func() print("After function call") return wrapper @my_decorator def say_hello(): print("Hello!") say_hello() # Output: # Before function call # Hello! # After function call # Decorator with arguments def repeat(times): def decorator(func): def wrapper(*args, **kwargs): for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Alice") # Prints "Hello, Alice!" three times # Practical decorator - timing functions import time def timer(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} took {end-start:.4f} seconds") return result return wrapper @timer def slow_function(): time.sleep(1) return "Done" slow_function()

Generators

Generators are functions that return an iterator using the yield keyword.

# Simple generator def count_up_to(n): count = 1 while count <= n: yield count count += 1 # Using the generator for num in count_up_to(5): print(num) # Prints 1, 2, 3, 4, 5 # Generator vs regular function # Regular function - uses memory for entire list def get_squares_list(n): result = [] for i in range(n): result.append(i ** 2) return result # Generator - yields one item at a time def get_squares_generator(n): for i in range(n): yield i ** 2 # Generator expression squares = (x**2 for x in range(10)) print(next(squares)) # 0 print(next(squares)) # 1 # Practical example - reading large files def read_large_file(file_path): with open(file_path, 'r') as file: for line in file: yield line.strip() # for line in read_large_file('big_file.txt'): # process(line) # Infinite generator def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b fib = fibonacci() for _ in range(10): print(next(fib))
Performance Tip: Use generators for large datasets to save memory. They generate values on-the-fly instead of storing everything in memory.

Context Managers

# Using context managers with open('file.txt', 'r') as file: content = file.read() # File automatically closed # Creating custom context manager class DatabaseConnection: def __init__(self, db_name): self.db_name = db_name self.connection = None def __enter__(self): print(f"Opening connection to {self.db_name}") self.connection = f"Connected to {self.db_name}" return self.connection def __exit__(self, exc_type, exc_val, exc_tb): print(f"Closing connection to {self.db_name}") self.connection = None return False with DatabaseConnection("mydb") as conn: print(f"Using {conn}") # Context manager with decorator from contextlib import contextmanager @contextmanager def temporary_setting(setting, value): old_value = setting setting = value try: yield setting finally: setting = old_value # with temporary_setting(config.debug, True): # # debug mode is True here # run_tests() # debug mode restored to original value

List Comprehensions and Lambda Functions

# Advanced list comprehensions matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flattened = [num for row in matrix for num in row] print(flattened) # [1, 2, 3, 4, 5, 6, 7, 8, 9] # Nested comprehension with condition result = [[x*y for x in range(1, 4)] for y in range(1, 4) if y % 2 == 0] # Lambda functions add = lambda x, y: x + y print(add(3, 5)) # 8 # Lambda with map, filter, reduce numbers = [1, 2, 3, 4, 5, 6] # Map - apply function to each item squared = list(map(lambda x: x**2, numbers)) print(squared) # [1, 4, 9, 16, 25, 36] # Filter - keep items that match condition evens = list(filter(lambda x: x % 2 == 0, numbers)) print(evens) # [2, 4, 6] # Reduce - combine items into single value from functools import reduce total = reduce(lambda x, y: x + y, numbers) print(total) # 21 # Sorting with lambda students = [ {"name": "Alice", "grade": 85}, {"name": "Bob", "grade": 92}, {"name": "Charlie", "grade": 78} ] sorted_students = sorted(students, key=lambda x: x["grade"]) print(sorted_students)

Python Best Practices

PEP 8 Style Guide:
  • Use 4 spaces for indentation (not tabs)
  • Maximum line length: 79 characters
  • Use snake_case for functions and variables
  • Use PascalCase for class names
  • Use UPPER_CASE for constants
  • Add docstrings to functions and classes
# Good code example def calculate_total_price(items, tax_rate=0.08): """ Calculate total price including tax. Args: items (list): List of item prices tax_rate (float): Tax rate as decimal Returns: float: Total price with tax """ subtotal = sum(items) tax = subtotal * tax_rate total = subtotal + tax return round(total, 2) # Constants MAX_RETRIES = 3 DEFAULT_TIMEOUT = 30 # Class with proper naming class UserAccount: """Represents a user account.""" def __init__(self, username, email): self.username = username self.email = email def send_notification(self, message): """Send notification to user.""" print(f"Sending to {self.email}: {message}")

Common Patterns and Idioms

# Swapping variables a, b = b, a # Chaining comparisons if 0 < x < 10: print("x is between 0 and 10") # Enumerate instead of range(len()) for i, item in enumerate(items): print(f"{i}: {item}") # Zip for parallel iteration names = ["Alice", "Bob", "Charlie"] ages = [25, 30, 35] for name, age in zip(names, ages): print(f"{name} is {age} years old") # Dict.get() with default value = my_dict.get('key', 'default_value') # List/dict unpacking first, *rest = [1, 2, 3, 4, 5] print(first) # 1 print(rest) # [2, 3, 4, 5] # String joining (efficient) words = ['Hello', 'World'] sentence = ' '.join(words) # Better than + in loops # Using any() and all() numbers = [2, 4, 6, 8] all_even = all(n % 2 == 0 for n in numbers) has_even = any(n % 2 == 0 for n in numbers)
Common Pitfalls:
  • Don't use mutable default arguments: def func(x=[])
  • Don't modify a list while iterating over it
  • Be careful with variable scope in loops
  • Don't catch all exceptions without good reason

Test Your Knowledge - Lesson 6

1. What keyword is used in a generator function to return values?

2. Which block in try-except-else-finally always executes?

3. What is the correct syntax for a lambda function that squares a number?