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)
# 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
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
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?