Format: 20 structured lessons, 15 minutes each. Complete at your own pace with hands-on coding exercises.
Master Python from zero to professional level in just 5 hours of focused learning. This self-study course is designed for maximum efficiency, covering computer science fundamentals, Python mastery, object-oriented programming, design patterns, testing, and real-world applications. Each 15-minute lesson includes theory, code examples, and practical exercises.
Course Structure:
Learning Outcomes:
Build professional applications including scalable web APIs, data pipelines, automation systems, and machine learning models. Master Python's ecosystem including Django, Flask, FastAPI, NumPy, Pandas, SQLAlchemy, and more. Learn industry best practices used at companies like Google, Microsoft, and Amazon.
Prerequisites: No programming experience required. Critical thinking and problem-solving mindset recommended.
Learning Outcomes: After completing this course, you'll be able to:
Each lesson is 15 minutes of focused learning with theory, examples, and exercises.
Below you'll find the complete material for all 20 lessons. Study at your own pace, practice with code examples, and complete the exercises.
Python is a programming language created in 1991 by Guido van Rossum. Think of it as a way to communicate with computers using English-like commands. It's one of the easiest programming languages to learn, which is why millions of beginners start here. Companies like Google, Netflix, and NASA use Python daily!
Why Python is popular: It reads almost like English, has a massive community for help, and can do everything from building websites to analyzing data to creating AI. You can write Python code in any text editor, but we recommend VS Code or PyCharm for beginners.
A variable is like a labeled box where you store information. You give it a name, and Python remembers what's inside. Think of it like this: if you have a box labeled "age" with the number 25 inside, whenever you ask for "age", Python gives you 25.
# Creating variables - it's as simple as: name = value
name = "Alice" # Text (called a "string")
age = 25 # Whole number (called an "integer")
height = 5.7 # Decimal number (called a "float")
is_student = True # True or False (called a "boolean")
# You can change variables anytime
age = 26 # Alice had a birthday!
print(age) # Output: 26
# Python is smart - it knows what type each variable is
print(type(name)) # means string
print(type(age)) # means integer
print(type(height)) # means floating-point number
print(type(is_student)) # means boolean
# Variable naming rules:
# โ
GOOD: user_name, total_price, age2
# โ BAD: 2age (can't start with number), user-name (no hyphens), class (reserved word)
Python can perform all basic math operations just like a calculator. Here's what each symbol means and how to use it:
# Addition (+) - adds two numbers
total = 10 + 5 # Result: 15
print(f"10 + 5 = {total}") # Output: 10 + 5 = 15
# Subtraction (-) - subtracts second from first
difference = 10 - 5 # Result: 5
print(f"10 - 5 = {difference}")
# Multiplication (*) - multiplies two numbers
product = 10 * 5 # Result: 50
print(f"10 ร 5 = {product}")
# Division (/) - always gives a decimal result
division = 10 / 3 # Result: 3.3333333...
print(f"10 รท 3 = {division}")
# Floor Division (//) - divides and rounds DOWN to nearest whole number
floor_div = 10 // 3 # Result: 3 (not 3.33...)
print(f"10 รท 3 (rounded down) = {floor_div}")
# Modulo (%) - gives the REMAINDER after division
# Think: "10 divided by 3 goes 3 times with 1 left over"
remainder = 10 % 3 # Result: 1 (the leftover)
print(f"10 mod 3 = {remainder}")
# Practical use: Check if number is even (num % 2 == 0)
# Exponentiation (**) - raises to a power
power = 2 ** 3 # Result: 8 (2 ร 2 ร 2)
squared = 5 ** 2 # Result: 25 (5 ร 5)
print(f"2ยณ = {power}")
print(f"5ยฒ = {squared}")
# Order of operations follows PEMDAS (like math class!)
# Parentheses, Exponents, Multiplication/Division, Addition/Subtraction
result = 2 + 3 * 4 # Result: 14 (not 20, because * goes first)
result2 = (2 + 3) * 4 # Result: 20 (parentheses first!)
# Practical example: Calculate total price with tax
price = 100
tax_rate = 0.08 # 8% tax
tax = price * tax_rate # Calculate tax amount: 8
total_price = price + tax # Add tax to price: 108
print(f"Price: ${price}, Tax: ${tax}, Total: ${total_price}")
Strings are sequences of characters (letters, numbers, symbols) surrounded by quotes. You can use single quotes ('text') or double quotes ("text") - they work the same. Strings are used for names, messages, emails, basically any text data.
# Creating strings - use quotes!
first_name = "Alice"
last_name = 'Smith' # Single quotes work too!
full_name = first_name + " " + last_name # Joining strings (concatenation)
print(full_name) # Output: Alice Smith
# String length - count characters
length = len(full_name) # Result: 11 (including the space!)
print(f"'{full_name}' has {length} characters")
# F-strings - the modern way to insert variables into text
# Put 'f' before the quotes and use {variable_name} inside
age = 25
message = f"{first_name} is {age} years old"
print(message) # Output: Alice is 25 years old
# You can even do math inside f-strings!
print(f"In 5 years, {first_name} will be {age + 5}")
# Output: In 5 years, Alice will be 30
# Old way (you'll see this in older code)
old_way = "My name is " + first_name + " and I am " + str(age) + " years old"
# F-strings are much cleaner!
# Multiline strings - use three quotes
long_text = """
This is a long message
that spans multiple lines.
Very useful for long descriptions!
"""
# Common string operations
text = "Hello World"
print(text.upper()) # HELLO WORLD (all uppercase)
print(text.lower()) # hello world (all lowercase)
print(text.replace("World", "Python")) # Hello Python
print(text.split()) # ['Hello', 'World'] (splits into list)
# Check if text contains something
email = "alice@email.com"
if "@" in email:
print("Valid email format!") # This will print
Booleans can only be True or False (notice the capital letters!). They're used for yes/no questions: Is the user logged in? Is the age valid? Did the payment succeed?
# Boolean values
is_student = True
is_graduated = False
has_license = True
# Comparison operators create booleans
age = 25
is_adult = age >= 18 # True (25 is greater than or equal to 18)
is_teenager = age < 20 # False (25 is NOT less than 20)
is_exactly_25 = age == 25 # True (equal comparison uses ==, not =)
is_not_30 = age != 30 # True (not equal to 30)
print(f"Is adult? {is_adult}") # Output: Is adult? True
print(f"Is teenager? {is_teenager}") # Output: Is teenager? False
# Combining conditions with AND, OR, NOT
has_id = True
is_over_21 = age >= 21
can_enter_club = has_id and is_over_21 # True (both must be true)
print(f"Can enter club? {can_enter_club}")
is_weekend = True
is_holiday = False
can_sleep_in = is_weekend or is_holiday # True (at least one is true)
print(f"Can sleep in? {can_sleep_in}")
is_awake = True
is_sleeping = not is_awake # False (opposite of True)
print(f"Is sleeping? {is_sleeping}")
Try these yourself:
age = 25 assigns a value, age == 25 comparesname = Alice โ should be name = "Alice" โ
"5" + 5 โ causes error. Use int("5") + 5 โ
Name and name are different variables!
Programs need to make decisions, just like humans. "If it's raining, bring an umbrella. Otherwise, don't." In Python, we use if, elif (else if), and else to create these decision branches.
Important: Python uses indentation (spaces or tabs) to know what code belongs to the if statement. This is different from many other languages! Always indent 4 spaces after a colon (:).
# Simple if statement - like asking a yes/no question
age = 20
if age >= 18:
print("You are an adult") # This runs if condition is True
print("You can vote") # Also part of the if block (indented)
print("This always runs") # Not indented = outside if block
# if-else - choose between two options
temperature = 75
if temperature > 80:
print("It's hot! ๐")
else:
print("It's nice out! ๐ค๏ธ")
# if-elif-else - multiple conditions (like a decision tree)
score = 85
if score >= 90: # First check
grade = "A"
print("Excellent! ๐")
elif score >= 80: # Only checked if first is False
grade = "B"
print("Great job! ๐")
elif score >= 70: # Only checked if above are False
grade = "C"
print("Good! ๐")
elif score >= 60:
grade = "D"
print("You passed ๐
")
else: # If all above are False
grade = "F"
print("Need to study more ๐")
print(f"Your grade is: {grade}")
# Real-world example: Login validation
username = "alice"
password = "secret123"
stored_username = "alice"
stored_password = "secret123"
if username == stored_username and password == stored_password:
print("โ
Login successful! Welcome!")
elif username == stored_username:
print("โ Incorrect password")
else:
print("โ User not found")
# Nested if statements - if inside if
age = 25
has_license = True
if age >= 18:
print("Old enough to drive")
if has_license:
print("You can drive! ๐")
else:
print("Get your license first! ๐")
else:
print("Too young to drive")
A for loop repeats code a specific number of times or goes through items in a list. Think of it as: "For each item in this collection, do something." It's perfect when you know how many times you want to repeat.
# range(n) - generates numbers from 0 to n-1
# range(5) gives you: 0, 1, 2, 3, 4
print("Counting to 5:")
for i in range(5):
print(f"Number: {i}") # Runs 5 times
# Output: Number: 0, Number: 1, Number: 2, Number: 3, Number: 4
# range(start, stop) - custom range
print("\nCounting from 1 to 5:")
for i in range(1, 6): # Starts at 1, stops before 6
print(i, end=" ") # end=" " prints on same line
# Output: 1 2 3 4 5
# range(start, stop, step) - count by different amounts
print("\nEven numbers:")
for i in range(0, 11, 2): # Count by 2s
print(i, end=" ")
# Output: 0 2 4 6 8 10
# Looping through lists - very common!
fruits = ["apple", "banana", "orange", "grape"]
print("\n\nFruits in my basket:")
for fruit in fruits: # 'fruit' takes each value in turn
print(f" - {fruit}")
# Get index AND value with enumerate()
print("\nNumbered list:")
for index, fruit in enumerate(fruits, start=1):
print(f"{index}. {fruit}")
# Output: 1. apple, 2. banana, etc.
# Practical example: Calculate total
prices = [10.99, 5.49, 3.99, 12.50]
total = 0
for price in prices:
total = total + price # Add each price to total
# Shorthand: total += price
print(f"Total: ${total:.2f}") # :.2f means 2 decimal places
# Nested loops - loop inside loop
print("\nMultiplication table:")
for i in range(1, 4): # Outer loop (rows)
for j in range(1, 4): # Inner loop (columns)
result = i * j
print(f"{i}ร{j}={result}", end=" ")
print() # New line after each row
# Output:
# 1ร1=1 1ร2=2 1ร3=3
# 2ร1=2 2ร2=4 2ร3=6
# 3ร1=3 3ร2=6 3ร3=9
A while loop keeps repeating as long as a condition is True. Use it when you don't know exactly how many times you need to repeat. Think: "While the door is locked, keep trying keys."
โ ๏ธ Warning: Make sure the condition eventually becomes False, or you'll create an infinite loop that never stops!
# Basic while loop
count = 0
while count < 5: # Keep going while count is less than 5
print(f"Count is: {count}")
count = count + 1 # IMPORTANT: Change the condition!
# Shorthand: count += 1
print("Loop finished!")
# Real example: User input validation
# Keep asking until user enters valid input
attempts = 0
max_attempts = 3
while attempts < max_attempts:
password = input("Enter password: ")
if password == "secret123":
print("โ
Access granted!")
break # Exit loop immediately
else:
attempts += 1
remaining = max_attempts - attempts
if remaining > 0:
print(f"โ Wrong! {remaining} attempts left")
else:
print("โ Account locked!")
# While True - intentional infinite loop (use with break!)
print("\nSimple calculator:")
while True:
user_input = input("Enter a number (or 'quit'): ")
if user_input.lower() == 'quit':
print("Goodbye!")
break # Only way to exit
number = int(user_input)
squared = number ** 2
print(f"{number} squared is {squared}")
# Countdown example
print("\nRocket launch countdown:")
seconds = 10
while seconds > 0:
print(seconds)
seconds -= 1
# In real program: time.sleep(1) would wait 1 second
print("๐ Blast off!")
Sometimes you need to skip an iteration or exit a loop early. break stops the loop completely. continue skips to the next iteration.
# break - exit loop immediately
print("Finding first even number:")
numbers = [1, 3, 5, 8, 9, 10, 12]
for num in numbers:
if num % 2 == 0: # Check if even
print(f"Found it: {num}")
break # Stop searching
print(f"{num} is odd, keep looking...")
# continue - skip to next iteration
print("\nPrint only positive numbers:")
numbers = [5, -2, 8, -1, 3, -4, 7]
for num in numbers:
if num < 0:
continue # Skip negative numbers
print(num) # Only prints positive ones
# Output: 5, 8, 3, 7
# Practical example: Process valid data only
print("\nProcessing emails:")
emails = ["alice@email.com", "invalid", "bob@email.com", "", "charlie@email.com"]
valid_count = 0
for email in emails:
if not email: # Skip empty strings
continue
if "@" not in email: # Skip invalid emails
print(f"โ ๏ธ Skipping invalid: {email}")
continue
# This code only runs for valid emails
print(f"โ
Sending to: {email}")
valid_count += 1
print(f"\nSent {valid_count} emails")
# else with loops - runs if loop completes WITHOUT break
print("\nSearching for 'python':")
words = ["java", "javascript", "ruby", "go"]
for word in words:
if word == "python":
print("Found Python!")
break
else:
# This runs because loop finished without finding python
print("Python not in list")
Try these yourself:
range(5) gives 0-4, not 1-5if x = 5 โ should be if x == 5 โ
Functions are like mini-programs inside your program. Instead of writing the same code multiple times, you write it once as a function and call it whenever needed. Think of a function like a recipe: you write the recipe once, then follow it any time you want to make that dish.
Benefits: Less code duplication, easier to test, easier to fix bugs (fix once, works everywhere), makes code more readable.
# Without functions - repetitive code ๐ซ
print("Welcome to our store!")
print("We're glad you're here!")
print("---")
print("Welcome to our store!")
print("We're glad you're here!")
print("---")
# With functions - DRY (Don't Repeat Yourself) ๐
def welcome_message():
"""Display welcome message to user."""
print("Welcome to our store!")
print("We're glad you're here!")
print("---")
# Now just call it
welcome_message() # First time
welcome_message() # Second time
# Much cleaner!
# Function anatomy:
# def = keyword to define function
# function_name = name you choose (use descriptive names!)
# () = parameters go here (empty if none)
# : = colon starts function body
# body = indented code that runs when function is called
# return = optional, sends data back to caller
Parameters are like placeholders in a function definition. Arguments are the actual values you pass when calling. Think of parameters as empty boxes in the recipe, and arguments as the ingredients you put in those boxes.
# Function with one parameter
def greet(name): # 'name' is the parameter
"""Greet a person by name."""
message = f"Hello, {name}! ๐"
print(message)
greet("Alice") # "Alice" is the argument
greet("Bob") # "Bob" is the argument
# Output:
# Hello, Alice! ๐
# Hello, Bob! ๐
# Multiple parameters
def introduce(name, age, city):
"""Introduce a person with their details."""
print(f"My name is {name}.")
print(f"I'm {age} years old.")
print(f"I live in {city}.")
print()
introduce("Alice", 25, "New York")
introduce("Bob", 30, "Los Angeles")
# Default parameter values - optional arguments
def order_coffee(size="medium", sugar=1):
"""Order coffee with optional customization."""
print(f"Ordering {size} coffee with {sugar} sugar(s)")
order_coffee() # Uses defaults: medium, 1 sugar
order_coffee("large") # Large, 1 sugar (uses default sugar)
order_coffee("small", 2) # Small, 2 sugars
order_coffee(sugar=3, size="large") # Named arguments - can be in any order!
# Real-world example: Calculate price with discount
def calculate_price(original_price, discount_percent=0):
"""Calculate final price after discount.
Args:
original_price: The original price before discount
discount_percent: Discount as percentage (default 0)
Returns:
Final price after discount
"""
discount_amount = original_price * (discount_percent / 100)
final_price = original_price - discount_amount
return final_price
# Usage
price1 = calculate_price(100) # No discount
price2 = calculate_price(100, 20) # 20% off
price3 = calculate_price(100, discount_percent=15) # Named argument
print(f"Regular: ${price1}")
print(f"20% off: ${price2}")
print(f"15% off: ${price3}")
The return statement sends a value back to wherever the function was called. Without return, the function does its work but gives you nothing back. With return, you can save and use the result.
# Function without return - just prints
def print_square(number):
result = number ** 2
print(f"{number} squared is {result}")
print_square(5) # Prints: 5 squared is 25
# But you can't save the result!
# Function with return - gives back a value
def calculate_square(number):
"""Calculate and return the square of a number."""
result = number ** 2
return result # Send result back
# Code after return never runs!
square = calculate_square(5) # Save the result
print(f"The square is: {square}")
# Now you can use it in calculations:
total = square + 10
print(f"Square + 10 = {total}")
# Return multiple values (actually returns a tuple)
def get_stats(numbers):
"""Calculate min, max, and average of a list."""
minimum = min(numbers)
maximum = max(numbers)
average = sum(numbers) / len(numbers)
return minimum, maximum, average # Return three values
data = [10, 20, 30, 40, 50]
min_val, max_val, avg_val = get_stats(data) # Unpack into 3 variables
print(f"Min: {min_val}")
print(f"Max: {max_val}")
print(f"Average: {avg_val}")
# Real-world example: Validate email
def is_valid_email(email):
"""Check if email address is valid (simple check).
Returns:
True if valid, False otherwise
"""
if "@" not in email:
return False # Early return if invalid
if "." not in email:
return False
if len(email) < 5:
return False
return True # All checks passed
# Usage with if statement
email1 = "alice@email.com"
email2 = "invalid"
if is_valid_email(email1):
print(f"{email1} is valid โ
")
else:
print(f"{email1} is invalid โ")
if is_valid_email(email2):
print(f"{email2} is valid โ
")
else:
print(f"{email2} is invalid โ")
# Return None explicitly (optional - functions return None by default)
def save_file(filename, data):
"""Save data to file (simplified example)."""
if not filename:
return None # Indicate failure
# In real program: write to file here
print(f"Saved to {filename}")
return True # Indicate success
result = save_file("data.txt", "Hello")
if result:
print("Save successful!")
else:
print("Save failed!")
Scope determines where a variable can be used. Variables created inside a function only exist inside that function (local scope). Variables outside functions can be used anywhere (global scope).
# Global variable - accessible everywhere
player_name = "Alice" # Global scope
def show_score():
score = 100 # Local variable - only exists in this function
print(f"{player_name} scored {score}")
show_score() # Works: can see player_name
# print(score) # ERROR! score doesn't exist here
# Function parameters are also local variables
def calculate(x, y):
result = x + y # x, y, and result are all local
return result
answer = calculate(5, 3)
# print(x) # ERROR! x only exists inside calculate()
# Modifying global variables (use sparingly!)
total_sales = 0 # Global variable
def make_sale(amount):
global total_sales # Tell Python we want to modify global
total_sales += amount
print(f"Sale recorded: ${amount}")
print(f"Total sales: ${total_sales}")
make_sale(50)
make_sale(75)
print(f"Final total: ${total_sales}") # 125
# Best practice: Return values instead of modifying globals
def add_to_total(current_total, amount):
"""Better approach - return new total instead of modifying global."""
return current_total + amount
my_total = 0
my_total = add_to_total(my_total, 50)
my_total = add_to_total(my_total, 75)
print(f"Final: ${my_total}")
Lambda functions are small, anonymous functions written in one line. Use them for simple operations, especially with functions like map(), filter(), and sorted().
# Regular function
def square(x):
return x ** 2
print(square(5)) # 25
# Same thing as lambda (anonymous function)
square_lambda = lambda x: x ** 2
print(square_lambda(5)) # 25
# Lambda with multiple parameters
multiply = lambda x, y: x * y
print(multiply(4, 5)) # 20
# Practical use: with map() to transform a list
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared) # [1, 4, 9, 16, 25]
# With filter() to select items
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4, 6, 8, 10]
# With sorted() for custom sorting
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78}
]
# Sort by grade
sorted_students = sorted(students, key=lambda s: s["grade"], reverse=True)
for student in sorted_students:
print(f"{student['name']}: {student['grade']}")
Always document your functions with docstrings (triple quotes). Explain what the function does, what parameters it takes, and what it returns. Future you will thank present you!
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
BMI = weight (kg) / height (m)ยฒ
Args:
weight_kg (float): Weight in kilograms
height_m (float): Height in meters
Returns:
float: BMI value rounded to 1 decimal place
Example:
>>> calculate_bmi(70, 1.75)
22.9
"""
bmi = weight_kg / (height_m ** 2)
return round(bmi, 1)
# View docstring
help(calculate_bmi) # Shows the documentation
print(calculate_bmi.__doc__) # Prints docstring
Try these yourself:
celsius_to_fahrenheit(celsius) that converts temperature. Formula: F = C ร 9/5 + 32is_even(number) that returns True if number is even, False otherwise.get_full_name(first, last) that returns the full name with proper capitalization.calculate_interest(principal, rate, years) with rate defaulting to 5%.greet (function object) vs greet() (calls function)return!def func(list=[]) - use None insteadLists are like containers that hold multiple items in order. Think of a shopping list, playlist, or to-do list. Lists are mutable (changeable) - you can add, remove, or modify items after creation.
# Creating lists
fruits = ["apple", "banana", "orange"] # List of strings
numbers = [1, 2, 3, 4, 5] # List of numbers
mixed = [1, "hello", 3.14, True] # Lists can mix types!
nested = [[1, 2], [3, 4], [5, 6]] # List of lists
empty = [] # Empty list
# Accessing items with indexing
# Index starts at 0 (first item is [0], not [1]!)
fruits = ["apple", "banana", "orange", "grape"]
print(fruits[0]) # apple (first item)
print(fruits[1]) # banana (second item)
print(fruits[-1]) # grape (last item)
print(fruits[-2]) # orange (second from end)
# Visual representation:
# Index: 0 1 2 3
# List: ["apple", "banana", "orange", "grape"]
# Neg: -4 -3 -2 -1
# Modifying lists
fruits[1] = "mango" # Change banana to mango
print(fruits) # ['apple', 'mango', 'orange', 'grape']
Slicing lets you extract a portion of a list. Syntax: list[start:stop:step]. Remember: it includes start but excludes stop.
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Basic slicing
print(numbers[2:5]) # [2, 3, 4] - from index 2 to 4
print(numbers[:3]) # [0, 1, 2] - from start to index 2
print(numbers[7:]) # [7, 8, 9] - from index 7 to end
print(numbers[:]) # [0, 1, 2, ...] - copy entire list
# Step parameter - take every nth item
print(numbers[::2]) # [0, 2, 4, 6, 8] - every 2nd item
print(numbers[1::2]) # [1, 3, 5, 7, 9] - odd numbers
print(numbers[::-1]) # [9, 8, 7, ... 0] - reverse list!
# Practical example: Get first 3 and last 3 items
scores = [95, 87, 92, 78, 88, 91, 85, 90, 93]
top_3 = scores[:3] # [95, 87, 92]
last_3 = scores[-3:] # [85, 90, 93]
Lists come with powerful built-in methods to add, remove, sort, and search for items.
# Adding items
fruits = ["apple", "banana"]
fruits.append("orange") # Add to end: ["apple", "banana", "orange"]
fruits.insert(1, "mango") # Insert at position 1: ["apple", "mango", "banana", "orange"]
fruits.extend(["grape", "kiwi"]) # Add multiple items
# Result: ["apple", "mango", "banana", "orange", "grape", "kiwi"]
# Removing items
fruits.remove("banana") # Remove specific item (first occurrence)
last_fruit = fruits.pop() # Remove and return last item
second_fruit = fruits.pop(1) # Remove and return item at index 1
fruits.clear() # Remove all items (empty list)
# Searching and counting
numbers = [1, 2, 3, 2, 4, 2, 5]
position = numbers.index(3) # Find position of first 3 (returns 2)
count = numbers.count(2) # How many 2s? (returns 3)
exists = 4 in numbers # Check if 4 exists (returns True)
# Sorting
numbers = [5, 2, 8, 1, 9, 3]
numbers.sort() # Sort in place: [1, 2, 3, 5, 8, 9]
numbers.sort(reverse=True) # Descending: [9, 8, 5, 3, 2, 1]
sorted_copy = sorted(numbers) # Create sorted copy (original unchanged)
words = ["zebra", "apple", "banana", "Cherry"]
words.sort() # Alphabetical: ['Cherry', 'apple', 'banana', 'zebra']
words.sort(key=str.lower) # Case-insensitive: ['apple', 'banana', 'Cherry', 'zebra']
# Real-world example: Shopping cart
cart = []
cart.append({"item": "laptop", "price": 999})
cart.append({"item": "mouse", "price": 25})
cart.append({"item": "keyboard", "price": 75})
# Sort by price
cart.sort(key=lambda x: x["price"])
print("Cheapest first:")
for item in cart:
print(f" {item['item']}: ${item['price']}")
List comprehensions create new lists in one concise line. They're faster and more readable than loops for simple transformations.
# Traditional way with loop
squares = []
for x in range(10):
squares.append(x ** 2)
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# List comprehension way - same result, one line!
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Syntax: [expression for item in iterable]
# With condition (filter)
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
names = ["alice", "bob", "charlie"]
capitalized = [name.capitalize() for name in names]
print(capitalized) # ['Alice', 'Bob', 'Charlie']
# More complex: multiple conditions
# Numbers divisible by both 2 and 3
divisible = [x for x in range(30) if x % 2 == 0 if x % 3 == 0]
print(divisible) # [0, 6, 12, 18, 24]
# Conditional expression (if-else in expression)
# Label numbers as even/odd
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
print(labels) # ['even', 'odd', 'even', 'odd', 'even']
# Practical example: Extract data from dictionaries
students = [
{"name": "Alice", "grade": 92},
{"name": "Bob", "grade": 87},
{"name": "Charlie", "grade": 95}
]
names = [s["name"] for s in students]
high_achievers = [s["name"] for s in students if s["grade"] >= 90]
print(high_achievers) # ['Alice', 'Charlie']
Tuples are like lists but immutable (unchangeable). Once created, you can't add, remove, or modify items. Use tuples for data that shouldn't change, like coordinates or RGB colors.
# Creating tuples
coordinates = (10, 20) # Tuple with parentheses
rgb_color = (255, 128, 0) # RGB: orange
person = ("Alice", 25, "Engineer") # Mixed types
single = (42,) # Single item - need comma!
# Note: (42) is just a number, (42,) is a tuple
# Accessing items (same as lists)
print(coordinates[0]) # 10
print(coordinates[-1]) # 20
# Unpacking - assign tuple values to variables
x, y = coordinates
print(f"x={x}, y={y}") # x=10, y=20
name, age, job = person
print(f"{name} is a {job}") # Alice is a Engineer
# Swap variables using tuple unpacking (Python magic!)
a = 5
b = 10
a, b = b, a # Swap in one line!
print(a, b) # 10 5
# Why use tuples?
# 1. Faster than lists
# 2. Protect data from accidental changes
# 3. Can be used as dictionary keys (lists can't)
# 4. Function can return multiple values as tuple
# Return multiple values from function
def get_min_max(numbers):
return min(numbers), max(numbers) # Returns tuple
minimum, maximum = get_min_max([3, 1, 4, 1, 5, 9])
print(f"Min: {minimum}, Max: {maximum}") # Min: 1, Max: 9
# Tuple methods (only 2!)
numbers = (1, 2, 3, 2, 4, 2, 5)
count = numbers.count(2) # How many 2s? (3)
index = numbers.index(4) # Position of first 4 (4)
Try these yourself:
words = ["python", "java", "javascript", "ruby"], create a list of lengths of each word.list2 = list1 doesn't copy - use list2 = list1.copy()(42,) โ
is tuple, (42) โ is just numberTopics: Dictionaries, dict methods, sets, set operations
# Dictionaries - key-value pairs
person = {
"name": "Alice",
"age": 25,
"city": "New York",
"skills": ["Python", "JavaScript"]
}
# Access and modify
print(person["name"]) # Alice
person["email"] = "alice@email.com" # Add new key
person["age"] = 26 # Update value
# Safe access with .get()
email = person.get("phone", "Not provided")
# Dictionary comprehension
squared_dict = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Iterate over dictionary
for key, value in person.items():
print(f"{key}: {value}")
# Sets - unordered collections of unique elements
fruits = {"apple", "banana", "orange"}
fruits.add("grape")
fruits.remove("banana")
# Set operations
set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}
union = set_a | set_b # {1, 2, 3, 4, 5, 6}
intersection = set_a & set_b # {3, 4}
difference = set_a - set_b # {1, 2}
Try these yourself:
.get() instead of [] when key might not exist{} โ is an empty dict, set() โ
is an empty setObject-Oriented Programming (OOP) is a way to organize code around "objects" - bundles of data and functions that work together. Think of a class as a cookie cutter (blueprint), and objects as the actual cookies you make from it.
Real-world analogy: A "Car" class is the blueprint defining what all cars have (color, model, speed) and can do (drive, brake, honk). Each specific car (your Honda, my Toyota) is an object created from that blueprint.
# Simple class definition
class Dog:
"""A simple class representing a dog."""
# The __init__ method runs when you create a new dog
# 'self' refers to the specific dog being created
def __init__(self, name, age):
"""Initialize a new dog with name and age."""
self.name = name # Instance attribute
self.age = age # Each dog has its own name/age
# Methods - functions that belong to the class
def bark(self):
"""Make the dog bark."""
return f"{self.name} says Woof!"
def get_info(self):
"""Return information about the dog."""
return f"{self.name} is {self.age} years old"
def birthday(self):
"""Celebrate birthday - increment age."""
self.age += 1
return f"Happy birthday {self.name}! Now {self.age} years old!"
# Create objects (instances) from the class
dog1 = Dog("Buddy", 3) # First dog object
dog2 = Dog("Max", 5) # Second dog object
# Each object has its own data!
# Call methods on objects
print(dog1.bark()) # Buddy says Woof!
print(dog2.bark()) # Max says Woof!
print(dog1.get_info()) # Buddy is 3 years old
# Modify object data
print(dog1.birthday()) # Happy birthday Buddy! Now 4 years old!
print(dog1.age) # 4
# Access attributes directly
dog1.name = "Buddy Jr."
print(dog1.name) # Buddy Jr.
Instance attributes (self.name, self.age) are unique to each object. Class attributes are shared by ALL objects of that class.
class Dog:
# Class attribute - shared by ALL dogs
species = "Canis familiaris"
total_dogs = 0 # Count all dogs created
def __init__(self, name, age):
# Instance attributes - unique to each dog
self.name = name
self.age = age
Dog.total_dogs += 1 # Increment class counter
def get_species(self):
return f"{self.name} is a {self.species}"
# Create dogs
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
# Class attribute - same for all
print(dog1.species) # Canis familiaris
print(dog2.species) # Canis familiaris
print(Dog.species) # Access via class name
# Instance attributes - different for each
print(dog1.name) # Buddy
print(dog2.name) # Max
# Check total dogs created
print(f"Total dogs: {Dog.total_dogs}") # Total dogs: 2
class BankAccount:
"""Represents a bank account with balance and transactions."""
def __init__(self, owner, balance=0):
"""Initialize account with owner and optional starting balance."""
self.owner = owner
self.balance = balance
self.transactions = [] # Track all transactions
def deposit(self, amount):
"""Add money to account."""
if amount <= 0:
return "Deposit must be positive!"
self.balance += amount
self.transactions.append(f"Deposit: +${amount}")
return f"Deposited ${amount}. New balance: ${self.balance}"
def withdraw(self, amount):
"""Remove money from account."""
if amount <= 0:
return "Withdrawal must be positive!"
if amount > self.balance:
return "Insufficient funds!"
self.balance -= amount
self.transactions.append(f"Withdrawal: -${amount}")
return f"Withdrew ${amount}. New balance: ${self.balance}"
def get_balance(self):
"""Get current balance."""
return f"{self.owner}'s balance: ${self.balance}"
def get_statement(self):
"""Print all transactions."""
print(f"\n=== Statement for {self.owner} ===")
for transaction in self.transactions:
print(f" {transaction}")
print(f"Current balance: ${self.balance}\n")
# Usage
account = BankAccount("Alice", 1000)
print(account.deposit(500)) # Deposited $500. New balance: $1500
print(account.withdraw(200)) # Withdrew $200. New balance: $1300
print(account.withdraw(2000)) # Insufficient funds!
account.get_statement()
# Output:
# Deposit: +$500
# Withdrawal: -$200
# Current balance: $1300
Special methods with double underscores (dunder methods) let you customize how objects behave with built-in Python operations.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
# __str__ - for print() and str()
def __str__(self):
return f"'{self.title}' by {self.author}"
# __repr__ - for debugging (official representation)
def __repr__(self):
return f"Book('{self.title}', '{self.author}', {self.pages})"
# __len__ - for len()
def __len__(self):
return self.pages
# __eq__ - for == comparison
def __eq__(self, other):
return self.title == other.title and self.author == other.author
book1 = Book("Python Basics", "John Doe", 300)
book2 = Book("Python Basics", "John Doe", 300)
book3 = Book("Advanced Python", "Jane Smith", 500)
print(book1) # 'Python Basics' by John Doe
print(len(book1)) # 300
print(book1 == book2) # True (same title/author)
print(book1 == book3) # False
Use underscore prefix to indicate "private" attributes (convention, not enforced). Use @property decorator to create controlled access.
class Person:
def __init__(self, name, age):
self.name = name
self._age = age # Single _ = "protected" (convention)
self.__password = "secret" # Double __ = "private" (name mangled)
# Property - access _age like an attribute
@property
def age(self):
"""Get age."""
return self._age
# Setter - control how age is set
@age.setter
def age(self, value):
"""Set age with validation."""
if value < 0 or value > 150:
raise ValueError("Age must be between 0 and 150")
self._age = value
def get_info(self):
return f"{self.name} is {self._age} years old"
person = Person("Alice", 25)
print(person.age) # 25 (uses @property getter)
person.age = 26 # Uses @age.setter
print(person.age) # 26
# Try to set invalid age
try:
person.age = -5 # ValueError!
except ValueError as e:
print(f"Error: {e}")
Try these yourself:
Car class with brand, model, year. Add methods: start_engine(), honk().Rectangle class with width and height. Add methods to calculate area and perimeter.Student class with name and grades list. Add methods: add_grade(), get_average().ShoppingCart class that can add items, remove items, and calculate total.Temperature class with Celsius property. Add conversion methods to Fahrenheit and Kelvin.selfname = "Alice" โ should be self.name = "Alice" โ
dog.bark โ should be dog.bark() โ
Topics: Inheritance, super(), method overriding, polymorphism
# Base class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass # Abstract method
# Derived classes
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
# Polymorphism - same method, different behavior
animals = [Dog("Buddy"), Cat("Whiskers"), Dog("Max")]
for animal in animals:
print(animal.speak())
# Using super() to extend parent behavior
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_info(self):
return f"{self.name}: ${self.salary}"
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary) # Call parent __init__
self.team_size = team_size
def get_info(self):
base_info = super().get_info()
return f"{base_info}, Team: {self.team_size}"
manager = Manager("Alice", 80000, 5)
print(manager.get_info())
Try these yourself:
Vehicle base class and inherit Car, Bicycle, Motorcycle from it.Shape class with Circle, Rectangle, Triangle children. Each calculates area differently.BankAccount and SavingsAccount (with interest) inheritance.Media class with Book, Movie, Music children. Each displays info differently.super().__init__() not ParentClass.__init__(self)
Decorators are functions that modify or enhance other functions. Think of them like gift wrapping: the gift (original function) stays the same, but you add something extra around it (wrapper). Use the @decorator_name syntax above a function.
Common uses: Logging, timing, authentication, caching, input validation.
# Basic decorator structure
def my_decorator(func):
"""Decorator that adds behavior before/after function."""
def wrapper(*args, **kwargs):
print("Before function runs")
result = func(*args, **kwargs) # Call original function
print("After function runs")
return result
return wrapper
# Apply decorator with @ syntax
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
# Output:
# Before function runs
# Hello, Alice!
# After function runs
# Without @ syntax (same thing):
def say_hi(name):
print(f"Hi, {name}!")
say_hi = my_decorator(say_hi) # Manual decoration
# Practical example: Timer decorator
def timer(func):
"""Measure how long a function takes to run."""
import time
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():
"""Simulates slow operation."""
import time
time.sleep(1)
return "Done!"
result = slow_function()
# Output: slow_function took 1.0004 seconds
# Decorator with arguments
def repeat(times):
"""Decorator that repeats function execution."""
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Bob")
# Output:
# Hello, Bob!
# Hello, Bob!
# Hello, Bob!
# Multiple decorators - stack them!
@timer
@repeat(2)
def process_data():
print("Processing...")
import time
time.sleep(0.5)
process_data()
# Runs twice, then times total duration
Generators produce values one at a time (on-demand) instead of creating entire lists in memory. Use yield instead of return. Perfect for large datasets, infinite sequences, or when you don't need all values at once.
Analogy: List = getting all books from library at once. Generator = checking out one book at a time as you read.
# Regular function - returns entire list (memory heavy)
def get_numbers_list(n):
"""Return list of n numbers."""
result = []
for i in range(n):
result.append(i)
return result # Returns complete list
numbers = get_numbers_list(1000000) # Uses lots of memory!
# Generator function - yields values one at a time (memory efficient)
def get_numbers_generator(n):
"""Generate numbers from 0 to n-1."""
for i in range(n):
yield i # Yields one value at a time
gen = get_numbers_generator(1000000) # Creates generator object (tiny memory!)
# Values are generated only when requested
# Using generator
for num in gen:
print(num) # Each number generated on-demand
if num >= 4: # Can stop early
break
# Output: 0, 1, 2, 3, 4
# Generator with yield - pauses and resumes
def countdown(n):
"""Count down from n to 1."""
print("Starting countdown")
while n > 0:
yield n # Pause here, return n
n -= 1 # Resume from here next time
print("Blast off!")
counter = countdown(5)
print(next(counter)) # 5 (calls next() manually)
print(next(counter)) # 4
print(next(counter)) # 3
# Or use in loop:
for num in countdown(3):
print(num)
# Output: 3, 2, 1, Blast off!
# Generator expression - like list comprehension but with ()
squares_list = [x**2 for x in range(1000000)] # List - uses ~8MB memory
squares_gen = (x**2 for x in range(1000000)) # Generator - uses ~200 bytes!
# Compare memory
import sys
print(f"List size: {sys.getsizeof(squares_list)} bytes")
print(f"Generator size: {sys.getsizeof(squares_gen)} bytes")
# Use generator
for square in squares_gen:
if square > 100:
break
print(square)
# Example 1: Read large file without loading all into memory
def read_large_file(filename):
"""Read file line by line - memory efficient for huge files."""
with open(filename, 'r') as file:
for line in file:
yield line.strip() # Yield one line at a time
# Usage - processes millions of lines without running out of memory
# for line in read_large_file('huge_log_file.txt'):
# if 'ERROR' in line:
# print(line)
# Example 2: Infinite sequence generator
def fibonacci():
"""Generate infinite Fibonacci sequence."""
a, b = 0, 1
while True: # Infinite loop!
yield a
a, b = b, a + b
# Get first 10 Fibonacci numbers
fib_gen = fibonacci()
for i in range(10):
print(next(fib_gen), end=' ')
# Output: 0 1 1 2 3 5 8 13 21 34
# Example 3: Data pipeline with generators
def read_csv_lines(filename):
"""Read CSV file."""
with open(filename) as f:
for line in f:
yield line.strip()
def parse_csv_line(lines):
"""Parse CSV lines into lists."""
for line in lines:
yield line.split(',')
def convert_to_dict(parsed_lines):
"""Convert to dictionaries."""
header = next(parsed_lines) # First line is header
for values in parsed_lines:
yield dict(zip(header, values))
# Chain generators - memory efficient pipeline!
# lines = read_csv_lines('data.csv')
# parsed = parse_csv_line(lines)
# records = convert_to_dict(parsed)
# for record in records:
# print(record)
# Example 4: Batch processing
def batch_generator(data, batch_size):
"""Split data into batches."""
for i in range(0, len(data), batch_size):
yield data[i:i + batch_size]
numbers = list(range(100))
for batch in batch_generator(numbers, 10):
print(f"Processing batch: {batch[:3]}... (size: {len(batch)})")
# Process batch
# Outputs 10 batches of 10 numbers each
Try these yourself:
@logger decorator that prints function name and arguments before execution.@cache decorator that stores function results to avoid recalculation.yield not return (return ends generator)
File I/O (Input/Output) lets programs read data from files and save data to files. Always use with statement to automatically close filesโeven if errors occur. Think of files as notebooks: you open, read/write, then close.
# Writing to a file - "w" mode (overwrites existing file)
with open("message.txt", "w") as file:
file.write("Hello, World!\n") # \n adds new line
file.write("Python is awesome!")
# File automatically closes when 'with' block ends
# Reading entire file - "r" mode
with open("message.txt", "r") as file:
content = file.read() # Read everything as string
print(content)
# Output:
# Hello, World!
# Python is awesome!
# Reading line by line (memory efficient for large files)
with open("message.txt", "r") as file:
for line in file:
print(line.strip()) # strip() removes \n
# Reading into list of lines
with open("message.txt", "r") as file:
lines = file.readlines() # Returns list
print(lines) # ['Hello, World!\n', 'Python is awesome!']
# Append mode - "a" adds to end without erasing
with open("message.txt", "a") as file:
file.write("\nNew line added!")
# File modes:
# "r" - Read (default, error if file doesn't exist)
# "w" - Write (creates new file, overwrites existing)
# "a" - Append (adds to end, creates if doesn't exist)
# "r+" - Read and write
# "rb" - Read binary (for images, videos, etc.)
# Check if file exists before reading
import os
if os.path.exists("message.txt"):
with open("message.txt", "r") as file:
print(file.read())
else:
print("File not found!")
# Write multiple lines at once
lines = ["First line", "Second line", "Third line"]
with open("output.txt", "w") as file:
file.write("\n".join(lines)) # Join with newlines
JSON (JavaScript Object Notation) is a universal format for storing structured data. Python dicts/lists convert perfectly to JSON. Used for APIs, config files, and data exchange between systems.
import json
# Python dict to JSON file
user_data = {
"name": "Alice",
"age": 25,
"email": "alice@email.com",
"skills": ["Python", "JavaScript", "SQL"],
"active": True
}
# Write JSON to file
with open("user.json", "w") as file:
json.dump(user_data, file, indent=2) # indent=2 makes it readable
# Creates formatted JSON file
# Read JSON from file
with open("user.json", "r") as file:
loaded_data = json.load(file) # Returns Python dict
print(loaded_data["name"]) # Alice
print(loaded_data["skills"]) # ['Python', 'JavaScript', 'SQL']
# Convert to/from JSON strings (for APIs)
json_string = json.dumps(user_data, indent=2) # Dict โ JSON string
print(json_string)
python_dict = json.loads(json_string) # JSON string โ Dict
print(python_dict)
# Real-world example: Config file
config = {
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp_db"
},
"debug": True,
"max_connections": 100
}
with open("config.json", "w") as file:
json.dump(config, file, indent=2)
# Load and use config
with open("config.json", "r") as file:
settings = json.load(file)
db_host = settings["database"]["host"]
print(f"Connecting to: {db_host}")
CSV (Comma-Separated Values) is a simple format for tabular data (like Excel). Each line is a row, commas separate columns. Perfect for data analysis and exchange with spreadsheets.
import csv
# Write CSV file
with open("employees.csv", "w", newline="") as file: # newline="" prevents extra blank lines
writer = csv.writer(file)
# Write header row
writer.writerow(["Name", "Age", "Department", "Salary"])
# Write data rows
writer.writerow(["Alice", 28, "Engineering", 75000])
writer.writerow(["Bob", 35, "Marketing", 65000])
writer.writerow(["Charlie", 42, "Sales", 80000])
# Read CSV file
with open("employees.csv", "r") as file:
reader = csv.reader(file)
header = next(reader) # Get first row (headers)
print(f"Headers: {header}")
for row in reader: # Each row is a list
name, age, dept, salary = row # Unpack values
print(f"{name} works in {dept}, earns ${salary}")
# CSV with DictReader - more convenient!
with open("employees.csv", "r") as file:
reader = csv.DictReader(file) # Each row is a dict
for row in reader:
print(f"{row['Name']} is {row['Age']} years old")
# CSV with DictWriter
employees = [
{"name": "Alice", "age": 28, "city": "NYC"},
{"name": "Bob", "age": 35, "city": "LA"}
]
with open("people.csv", "w", newline="") as file:
fieldnames = ["name", "age", "city"]
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader() # Write column names
writer.writerows(employees) # Write all rows
Exceptions are errors that occur during program execution. Use try/except to catch and handle errors gracefully instead of crashing. Like a safety net for your code.
# Basic try/except
try:
result = 10 / 0 # This will cause an error
except ZeroDivisionError:
print("Cannot divide by zero!") # Handles specific error
result = 0
print(f"Result: {result}") # Program continues
# Multiple exception types
try:
number = int(input("Enter a number: "))
result = 100 / number
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
except Exception as e: # Catch any other error
print(f"Unexpected error: {e}")
# try/except/else/finally - complete structure
try:
file = open("data.txt", "r")
data = file.read()
except FileNotFoundError:
print("File not found!")
data = None
else:
print("File read successfully!") # Runs if NO exception
finally:
print("Cleanup code") # ALWAYS runs
# file.close() # Usually done with 'with' instead
# Real example: Safe file reading
def read_config(filename):
"""Read JSON config file safely."""
try:
with open(filename, "r") as file:
config = json.load(file)
return config
except FileNotFoundError:
print(f"Config file '{filename}' not found. Using defaults.")
return {"debug": False, "port": 8000} # Default config
except json.JSONDecodeError:
print(f"Invalid JSON in '{filename}'")
return None
config = read_config("settings.json")
# Raising exceptions
def withdraw(balance, amount):
"""Withdraw money from account."""
if amount < 0:
raise ValueError("Cannot withdraw negative amount!")
if amount > balance:
raise ValueError("Insufficient funds!")
return balance - amount
try:
new_balance = withdraw(100, 150)
except ValueError as e:
print(f"Transaction failed: {e}")
# Custom exceptions
class InsufficientFundsError(Exception):
"""Custom exception for banking operations."""
pass
def withdraw_v2(balance, amount):
if amount > balance:
raise InsufficientFundsError(f"Need ${amount}, only have ${balance}")
return balance - amount
try:
withdraw_v2(100, 150)
except InsufficientFundsError as e:
print(f"Error: {e}")
Try these yourself:
except:Modules are Python files containing reusable code (functions, classes, variables). Instead of copying code between projects, put it in a module and import it. Think of modules as toolboxes: each contains specific tools you can use.
Python Standard Library: Python comes with 200+ built-in modules (math, random, datetime, etc.) - no installation needed!
# Import entire module
import math
print(math.pi) # 3.141592653589793
print(math.sqrt(16)) # 4.0
print(math.floor(3.7)) # 3
print(math.ceil(3.2)) # 4
# Import specific items
from math import sqrt, pi, pow
print(sqrt(25)) # 5.0 (no math. prefix needed)
print(pi) # 3.141592...
# Import with alias (shorter names)
import datetime as dt
now = dt.datetime.now()
print(now)
# Common standard library modules
import random
print(random.randint(1, 10)) # Random number 1-10
print(random.choice(['red', 'blue', 'green'])) # Random choice
import os
print(os.getcwd()) # Current directory
# os.mkdir('new_folder') # Create directory
import sys
print(sys.version) # Python version
print(sys.platform) # Operating system
from datetime import datetime, timedelta
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S")) # Format date
tomorrow = now + timedelta(days=1)
print(f"Tomorrow: {tomorrow}")
# Import everything (not recommended - pollutes namespace)
# from math import * # Imports all, hard to track
Any Python file (.py) is a module! Save functions/classes in a file, then import it from other files in the same directory.
# File: utils.py (your module)
"""Utility functions for common tasks."""
def greet(name):
"""Return greeting message."""
return f"Hello, {name}!"
def add(a, b):
"""Add two numbers."""
return a + b
def calculate_tax(price, tax_rate=0.08):
"""Calculate price with tax."""
return price * (1 + tax_rate)
# Constants
PI = 3.14159
MAX_USERS = 100
# Private function (convention: starts with _)
def _internal_helper():
"""Not meant to be used outside this module."""
pass
# Code that runs only when module is executed directly
if __name__ == "__main__":
# This runs when you do: python utils.py
# But NOT when you import utils
print("Testing utils module...")
print(greet("Alice"))
print(add(5, 3))
# --------------------------------
# File: main.py (using the module)
import utils
print(utils.greet("Bob")) # Hello, Bob!
result = utils.add(10, 20) # 30
total = utils.calculate_tax(100) # 108.0
print(f"Max users: {utils.MAX_USERS}") # 100
# Or import specific items
from utils import greet, add
print(greet("Charlie"))
print(add(7, 8))
# Import with alias
import utils as u
print(u.greet("Diana"))
Packages are directories containing multiple modules. Create a folder with an __init__.py file (can be empty) to make it a package.
# Package structure:
myapp/
__init__.py # Makes it a package (can be empty or have init code)
database.py # Module for database operations
auth.py # Module for authentication
utils.py # Module for utilities
models/ # Subpackage
__init__.py
user.py
product.py
# File: myapp/database.py
def connect():
return "Connected to database"
def query(sql):
return f"Executing: {sql}"
# File: myapp/auth.py
def login(username, password):
return f"Logging in {username}"
def logout():
return "Logged out"
# File: myapp/__init__.py (optional initialization)
"""MyApp package - main application package."""
VERSION = "1.0.0"
# --------------------------------
# Using the package:
# Import from package
from myapp import database
print(database.connect())
# Import specific function
from myapp.auth import login
print(login("alice", "secret"))
# Import from subpackage
from myapp.models.user import User
# Import entire package
import myapp
print(f"Version: {myapp.VERSION}")
# Relative imports (inside package files)
# In myapp/auth.py:
# from .database import connect # Same level
# from ..utils import helper # Parent level
# from .models.user import User # Subdirectory
pip is Python's package installer. Access 400,000+ packages from PyPI (Python Package Index). Popular packages: requests, pandas, numpy, django, flask.
# Install package (run in terminal/command prompt)
# pip install requests
# pip install pandas numpy matplotlib
# pip install django==4.2.0 # Specific version
# Uninstall
# pip uninstall requests
# List installed packages
# pip list
# Show package info
# pip show requests
# Save dependencies to file
# pip freeze > requirements.txt
# Install from requirements file
# pip install -r requirements.txt
# Example: Using installed package
import requests
response = requests.get('https://api.github.com')
print(response.status_code) # 200
data = response.json()
print(data)
# Example with pandas
import pandas as pd
data = {
'Name': ['Alice', 'Bob', 'Charlie'],
'Age': [25, 30, 35]
}
df = pd.DataFrame(data)
print(df)
Virtual environments create isolated Python installations for each project. Different projects can use different package versions without conflicts. Essential for professional development!
# Create virtual environment (terminal/command prompt)
# python -m venv myenv # Creates 'myenv' folder
# Activate virtual environment
# Windows:
# myenv\Scripts\activate
# Mac/Linux:
# source myenv/bin/activate
# When activated, you'll see (myenv) in terminal
# (myenv) $
# Now pip install only affects this environment
# (myenv) $ pip install requests pandas
# Deactivate when done
# (myenv) $ deactivate
# Best practice: .gitignore
"""
# Add to .gitignore:
myenv/
venv/
*.pyc
__pycache__/
"""
# Project structure with venv
myproject/
myenv/ # Virtual environment (don't commit to git!)
src/
main.py
utils.py
requirements.txt # List of dependencies
README.md
# requirements.txt example:
"""
requests==2.31.0
pandas==2.0.3
numpy==1.24.3
"""
# Workflow
# 1. Create project folder
# 2. Create virtual environment: python -m venv venv
# 3. Activate: venv\Scripts\activate (Windows)
# 4. Install packages: pip install requests pandas
# 5. Save deps: pip freeze > requirements.txt
# 6. Code your project
# 7. Share: Others run pip install -r requirements.txt
Try these yourself:
Big O notation describes how code performance changes as input size grows. It answers: "How much slower will my code be with 10ร more data?" Essential for writing scalable applications that work with thousands/millions of items.
Analogy: Imagine counting books. O(1) = check if shelf is empty (instant). O(n) = count all books one by one (linear). O(nยฒ) = compare every book with every other book (exponential growth).
# Common Time Complexities (from best to worst):
# O(1) - CONSTANT - Always same time regardless of input size
def get_first_item(items):
"""Access list element - instant!"""
return items[0] # Same speed for 10 or 10 million items
# O(log n) - LOGARITHMIC - Doubles data, adds one step
def binary_search(sorted_list, target):
"""Search sorted list by halving search space."""
left, right = 0, len(sorted_list) - 1
while left <= right:
mid = (left + right) // 2
if sorted_list[mid] == target:
return mid # Found it!
elif sorted_list[mid] < target:
left = mid + 1 # Search right half
else:
right = mid - 1 # Search left half
return -1 # Not found
# Example: Find 47 in [1,5,12,23,47,59,81,99]
# Step 1: Check middle (23) - too small
# Step 2: Check middle of right half (59) - too big
# Step 3: Check middle of that section (47) - found!
# Only 3 steps for 8 items, 10 steps for 1024 items!
# O(n) - LINEAR - Time grows with input size
def find_max(numbers):
"""Must check every number."""
max_val = numbers[0]
for num in numbers: # Visit each item once
if num > max_val:
max_val = num
return max_val
# 100 items = 100 operations
# 1000 items = 1000 operations (10ร slower)
# O(n log n) - LINEARITHMIC - Efficient sorting
def merge_sort(arr):
"""Divide and conquer sorting."""
if len(arr) <= 1:
return arr
# Divide
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # Sort left half
right = merge_sort(arr[mid:]) # Sort right half
# Conquer (merge sorted halves)
return merge(left, right)
def merge(left, right):
"""Merge two sorted lists."""
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:]) # Add remaining
result.extend(right[j:])
return result
# O(nยฒ) - QUADRATIC - Nested loops (avoid for large data!)
def bubble_sort(arr):
"""Compare adjacent pairs - slow for large lists."""
n = len(arr)
for i in range(n): # Outer loop
for j in range(n - i - 1): # Inner loop
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j] # Swap
return arr
# 100 items = 10,000 operations
# 1000 items = 1,000,000 operations (100ร slower!)
# Visual comparison of growth:
# n = 10 n = 100 n = 1000
# O(1): 1 1 1 โ
Best
# O(log n): 3 7 10 โ
Great
# O(n): 10 100 1000 โ
Good
# O(n log n): 33 664 9966 โ ๏ธ OK
# O(nยฒ): 100 10,000 1,000,000 โ Slow
# Linear Search - O(n) - Check each item
def linear_search(items, target):
"""Simple but slow for large lists."""
for i, item in enumerate(items):
if item == target:
return i # Found at index i
return -1 # Not found
numbers = [64, 34, 25, 12, 22, 11, 90]
print(linear_search(numbers, 22)) # 4
# Binary Search - O(log n) - MUCH faster but needs sorted list!
def binary_search_recursive(arr, target, left, right):
"""Recursive version."""
if left > right:
return -1 # Base case: not found
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, right)
else:
return binary_search_recursive(arr, target, left, mid - 1)
sorted_nums = [11, 12, 22, 25, 34, 64, 90]
print(binary_search_recursive(sorted_nums, 25, 0, len(sorted_nums)-1)) # 3
# Bubble Sort - O(nยฒ) - Simple but slow
def bubble_sort(arr):
"""Swap adjacent elements if out of order."""
n = len(arr)
for i in range(n):
swapped = False
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped: # Already sorted
break
return arr
# Selection Sort - O(nยฒ) - Find min, move to front
def selection_sort(arr):
"""Find minimum, place at beginning."""
for i in range(len(arr)):
min_idx = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
# Quick Sort - O(n log n) average - Very fast in practice
def quick_sort(arr):
"""Pick pivot, partition around it."""
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
# Python's built-in sort() and sorted() use Timsort
# Timsort - O(n log n) - Hybrid of merge + insertion sort
numbers = [64, 34, 25, 12, 22, 11, 90]
numbers.sort() # In-place sort
sorted_nums = sorted(numbers) # Returns new list
# Comparison:
import time
import random
data = [random.randint(1, 1000) for _ in range(1000)]
# Bubble sort (slow)
start = time.time()
bubble_sort(data.copy())
print(f"Bubble: {time.time() - start:.4f}s")
# Quick sort (fast)
start = time.time()
quick_sort(data.copy())
print(f"Quick: {time.time() - start:.4f}s")
# Python built-in (fastest)
start = time.time()
sorted(data)
print(f"Built-in: {time.time() - start:.4f}s")
Try these yourself:
Design patterns are proven solutions to common programming problems. Like architectural blueprints, they provide templates for writing maintainable, scalable code. Originally documented in the "Gang of Four" book (1994).
Analogy: Cooking recipes. You don't invent how to make bread from scratchโyou follow a proven pattern that works every time.
Problem: You need exactly ONE instance of a class (database connection, config, logger).
# Singleton Pattern - Ensures only one instance exists
class Database:
"""Database connection - only need one!"""
_instance = None # Class variable to store instance
def __new__(cls):
"""Control object creation."""
if cls._instance is None:
print("Creating new database connection...")
cls._instance = super().__new__(cls)
cls._instance.connection = "Connected to MySQL"
cls._instance.queries = []
return cls._instance # Always return same instance
def execute_query(self, query):
"""Execute SQL query."""
self.queries.append(query)
print(f"Executing: {query}")
# Test Singleton behavior
db1 = Database() # Creates instance
db2 = Database() # Returns SAME instance
print(db1 is db2) # True - same object!
print(id(db1) == id(db2)) # True - same memory address
db1.execute_query("SELECT * FROM users")
print(db2.queries) # ['SELECT * FROM users'] - shared state!
# Real-world use cases:
# - Database connections (avoid multiple connections)
# - Configuration settings (one config for entire app)
# - Logging service (centralized logging)
# - Thread pools (manage limited resources)
Problem: You need to create objects without knowing exact class names in advance.
# Factory Pattern - Create objects based on conditions
from abc import ABC, abstractmethod
# Define interface (contract)
class Animal(ABC):
"""All animals must be able to speak."""
@abstractmethod
def speak(self):
pass
@abstractmethod
def move(self):
pass
# Concrete implementations
class Dog(Animal):
def speak(self):
return "Woof! Woof!"
def move(self):
return "Running on four legs"
class Cat(Animal):
def speak(self):
return "Meow!"
def move(self):
return "Sneaking silently"
class Bird(Animal):
def speak(self):
return "Chirp! Chirp!"
def move(self):
return "Flying in the sky"
# Factory - Creates objects without exposing creation logic
class AnimalFactory:
"""Factory to create animals."""
@staticmethod
def create_animal(animal_type):
"""Create animal based on type string."""
animals = {
"dog": Dog,
"cat": Cat,
"bird": Bird
}
animal_class = animals.get(animal_type.lower())
if animal_class:
return animal_class()
else:
raise ValueError(f"Unknown animal type: {animal_type}")
# Usage - Clean and flexible!
def interact_with_animal(animal_type):
"""Create and interact with any animal."""
animal = AnimalFactory.create_animal(animal_type)
print(f"{animal_type.title()}: {animal.speak()}")
print(f"Movement: {animal.move()}\n")
interact_with_animal("dog") # Create dog
interact_with_animal("cat") # Create cat
interact_with_animal("bird") # Create bird
# Add new animal without changing existing code!
class Fish(Animal):
def speak(self):
return "Blub blub"
def move(self):
return "Swimming underwater"
# Just register in factory dictionary
Problem: You need to notify multiple objects when something changes (event system).
# Observer Pattern - Publish/Subscribe mechanism
class Subject:
"""Observable object - notifies observers of changes."""
def __init__(self, name):
self.name = name
self._observers = [] # List of observers
self._state = None
def attach(self, observer):
"""Subscribe observer to notifications."""
if observer not in self._observers:
self._observers.append(observer)
print(f"{observer.name} subscribed to {self.name}")
def detach(self, observer):
"""Unsubscribe observer."""
self._observers.remove(observer)
print(f"{observer.name} unsubscribed from {self.name}")
def notify(self, message):
"""Notify all observers."""
print(f"\n๐ข {self.name}: Broadcasting '{message}'")
for observer in self._observers:
observer.update(message)
def change_state(self, new_state):
"""Change state and notify observers."""
self._state = new_state
self.notify(f"State changed to: {new_state}")
# Concrete Observers
class EmailObserver:
"""Sends email notifications."""
def __init__(self, email):
self.name = "EmailService"
self.email = email
def update(self, message):
print(f" ๐ง Email to {self.email}: {message}")
class SMSObserver:
"""Sends SMS notifications."""
def __init__(self, phone):
self.name = "SMSService"
self.phone = phone
def update(self, message):
print(f" ๐ฑ SMS to {self.phone}: {message}")
class LogObserver:
"""Logs all events."""
def __init__(self):
self.name = "Logger"
def update(self, message):
print(f" ๐ LOG: {message}")
# Real-world example: Online store
store = Subject("OnlineStore")
# Subscribe observers
email_service = EmailObserver("user@example.com")
sms_service = SMSObserver("+1234567890")
logger = LogObserver()
store.attach(email_service)
store.attach(sms_service)
store.attach(logger)
# Trigger events - all observers notified automatically!
store.notify("Flash Sale: 50% off!")
store.change_state("New order received")
store.notify("Your package has shipped!")
# Unsubscribe SMS
store.detach(sms_service)
store.notify("Order delivered!") # SMS won't receive this
# Use cases:
# - News websites (notify subscribers of new articles)
# - Stock market apps (price change alerts)
# - Social media (notification system)
# - Game engines (event handling)
Try these yourself:
SOLID is an acronym for 5 design principles that make software more maintainable, flexible, and scalable. Created by Robert C. Martin (Uncle Bob). These principles prevent "code rot" and make systems easier to change.
Analogy: Building a house. SOLID principles are like architectural rulesโuse strong foundations, separate plumbing from electricity, make rooms easy to renovate.
Rule: A class should have ONE reason to change. Each class should do ONE thing well.
# โ BAD - Class doing too many things
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
"""Database logic - wrong place!"""
print(f"Saving {self.name} to database")
def send_email(self):
"""Email logic - wrong place!"""
print(f"Sending email to {self.email}")
def generate_report(self):
"""Report logic - wrong place!"""
print(f"Generating report for {self.name}")
# Problem: If email system changes, User class must change!
# Problem: User class has 3 reasons to change (data, DB, email, reports)
# โ
GOOD - Each class has ONE responsibility
class User:
"""Only handles user data."""
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
"""Only handles database operations."""
def save(self, user):
print(f"Saving {user.name} to database")
# Database logic here
def find_by_email(self, email):
print(f"Finding user with email: {email}")
# Query logic here
class EmailService:
"""Only handles email sending."""
def send_welcome_email(self, user):
print(f"๐ง Welcome email sent to {user.email}")
# Email logic here
def send_password_reset(self, user):
print(f"๐ง Password reset sent to {user.email}")
class ReportGenerator:
"""Only handles report generation."""
def generate_user_report(self, user):
print(f"๐ Generating report for {user.name}")
# Report logic here
# Usage - Clean separation of concerns!
user = User("Alice", "alice@example.com")
repository = UserRepository()
email_service = EmailService()
report_gen = ReportGenerator()
repository.save(user)
email_service.send_welcome_email(user)
report_gen.generate_user_report(user)
# Benefits:
# - Easy to test each class independently
# - Easy to change email logic without touching User class
# - Easy to reuse EmailService for other entities
Rule: Classes should be OPEN for extension but CLOSED for modification. Add new features without changing existing code.
# โ BAD - Must modify class to add new discount types
class DiscountCalculator:
def calculate(self, amount, discount_type):
if discount_type == "seasonal":
return amount * 0.9
elif discount_type == "vip":
return amount * 0.8
elif discount_type == "student": # Need to modify!
return amount * 0.85
# Adding new discount = modifying this class โ
# โ
GOOD - Use abstraction to extend behavior
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
"""Abstract base for all discount strategies."""
@abstractmethod
def calculate(self, amount):
pass
class SeasonalDiscount(DiscountStrategy):
"""Seasonal sale discount."""
def calculate(self, amount):
return amount * 0.9 # 10% off
class VIPDiscount(DiscountStrategy):
"""VIP customer discount."""
def calculate(self, amount):
return amount * 0.8 # 20% off
class StudentDiscount(DiscountStrategy):
"""Student discount - NEW, no modification needed!"""
def calculate(self, amount):
return amount * 0.85 # 15% off
class BlackFridayDiscount(DiscountStrategy):
"""Black Friday discount - Also NEW!"""
def calculate(self, amount):
return amount * 0.5 # 50% off!
# Discount calculator - NEVER needs modification
class ShoppingCart:
def __init__(self):
self.items = []
self.discount_strategy = None
def add_item(self, price):
self.items.append(price)
def set_discount(self, strategy: DiscountStrategy):
"""Set discount strategy."""
self.discount_strategy = strategy
def get_total(self):
"""Calculate total with discount."""
total = sum(self.items)
if self.discount_strategy:
return self.discount_strategy.calculate(total)
return total
# Usage - Easy to add new discounts!
cart = ShoppingCart()
cart.add_item(100)
cart.add_item(50)
cart.set_discount(VIPDiscount())
print(f"VIP Total: ${cart.get_total()}") # $120
cart.set_discount(BlackFridayDiscount())
print(f"Black Friday: ${cart.get_total()}") # $75
# Benefits:
# - Add new discounts without modifying ShoppingCart
# - Easy to test each discount in isolation
# - Following "D" in SOLID (next!)
Rule: Subclasses should be substitutable for their parent classes without breaking the program.
# โ BAD - Square violates LSP
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
"""Square - width always equals height."""
def set_width(self, width):
self.width = width
self.height = width # Problem!
def set_height(self, height):
self.width = height # Problem!
self.height = height
# Test with Rectangle
def test_rectangle(rect):
rect.set_width(5)
rect.set_height(10)
assert rect.area() == 50 # 5 * 10 = 50
rect = Rectangle(0, 0)
test_rectangle(rect) # โ
Works!
square = Square(0, 0)
test_rectangle(square) # โ FAILS! area = 100, not 50
# โ
GOOD - Use composition, not inheritance
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract shape."""
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
# Now both work correctly without substitution issues!
Rule: Don't force classes to implement methods they don't need. Use small, focused interfaces.
# โ BAD - Fat interface
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
class HumanWorker(Worker):
def work(self):
print("Human working")
def eat(self):
print("Human eating lunch")
class RobotWorker(Worker):
def work(self):
print("Robot working")
def eat(self):
"""Robots don't eat!"""
raise NotImplementedError("Robots don't eat") # Problem!
# โ
GOOD - Segregated interfaces
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class HumanWorker(Workable, Eatable):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
class RobotWorker(Workable): # Only implements Workable
def work(self):
print("Robot working 24/7")
# Now robots aren't forced to implement eat()!
Rule: Depend on abstractions, not concrete implementations. High-level modules shouldn't depend on low-level modules.
# โ BAD - Direct dependency on concrete class
class MySQLDatabase:
def save(self, data):
print(f"Saving to MySQL: {data}")
class UserService:
def __init__(self):
self.database = MySQLDatabase() # Tightly coupled!
def create_user(self, name):
self.database.save(name)
# Problem: Hard to switch to PostgreSQL or test with mock DB
# โ
GOOD - Depend on abstraction
from abc import ABC, abstractmethod
class Database(ABC):
"""Abstract database interface."""
@abstractmethod
def save(self, data): pass
@abstractmethod
def find(self, id): pass
class MySQLDatabase(Database):
def save(self, data):
print(f"๐พ Saving to MySQL: {data}")
def find(self, id):
print(f"๐ Finding in MySQL: {id}")
class PostgreSQLDatabase(Database):
def save(self, data):
print(f"๐พ Saving to PostgreSQL: {data}")
def find(self, id):
print(f"๐ Finding in PostgreSQL: {id}")
class MockDatabase(Database):
"""For testing."""
def save(self, data):
print(f"โ
Mock save: {data}")
def find(self, id):
return {"id": id, "name": "Test User"}
class UserService:
def __init__(self, database: Database): # Depends on abstraction!
self.database = database
def create_user(self, name):
self.database.save(name)
def get_user(self, id):
return self.database.find(id)
# Usage - Easy to swap implementations!
mysql_service = UserService(MySQLDatabase())
mysql_service.create_user("Alice")
postgres_service = UserService(PostgreSQLDatabase())
postgres_service.create_user("Bob")
test_service = UserService(MockDatabase()) # For unit tests!
test_service.create_user("TestUser")
Try these yourself:
Automated testing is your safety netโcatch bugs before users do! Tests document how code should work, make refactoring safe, and save debugging time.
Topics: Unit tests, pytest, assertions, fixtures, TDD
# Install pytest: pip install pytest
# test_calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# Test functions (prefix with test_)
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_subtract():
assert subtract(5, 3) == 2
assert subtract(0, 5) == -5
def test_divide():
assert divide(10, 2) == 5
assert divide(9, 3) == 3
def test_divide_by_zero():
import pytest
with pytest.raises(ValueError):
divide(10, 0)
# Fixtures - reusable test setup
import pytest
@pytest.fixture
def sample_data():
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
assert sum(sample_data) == 15
def test_max(sample_data):
assert max(sample_data) == 5
# Run tests: pytest test_calculator.py
# Run with coverage: pytest --cov=mymodule
# Test-Driven Development (TDD)
# 1. Write failing test
# 2. Write minimal code to pass
# 3. Refactor
API (Application Programming Interface) lets programs talk to each other. REST APIs use HTTP to send/receive data (usually JSON). Essential for modern web development!
Analogy: APIs are like restaurant menusโyou order (request) something specific, kitchen (server) prepares it, waiter (HTTP) brings it back (response).
# Install: pip install requests
import requests
import json
# GET - Retrieve data
response = requests.get('https://api.github.com/users/octocat')
print(response.status_code) # 200 = success
print(response.json()) # Parse JSON response
# POST - Send data
data = {'name': 'Alice', 'email': 'alice@email.com'}
response = requests.post('https://api.example.com/users', json=data)
print(response.json())
# Query parameters
params = {'q': 'python', 'sort': 'stars'}
response = requests.get('https://api.github.com/search/repositories', params=params)
# Headers & Authentication
headers = {'Authorization': 'Bearer YOUR_TOKEN'}
response = requests.get('https://api.example.com/protected', headers=headers)
Try these yourself:
# Install: pip install beautifulsoup4 requests lxml
from bs4 import BeautifulSoup
import requests
# Fetch webpage
url = 'https://example.com'
response = requests.get(url)
soup = BeautifulSoup(response.content, 'lxml')
# Extract data
title = soup.find('h1').text
links = [a['href'] for a in soup.find_all('a')]
paragraphs = [p.text for p in soup.find_all('p')]
print(f"Title: {title}")
print(f"Links: {links}")
Try these yourself:
# Install: pip install pandas matplotlib
import pandas as pd
import matplotlib.pyplot as plt
# Create DataFrame
data = {
'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'Age': [25, 30, 35, 28],
'Salary': [50000, 60000, 75000, 55000]
}
df = pd.DataFrame(data)
# Basic operations
print(df.head()) # First 5 rows
print(df.describe()) # Statistics
print(df['Age'].mean()) # Average age
# Filtering
high_salary = df[df['Salary'] > 55000]
print(high_salary)
# Grouping
avg_by_age = df.groupby('Age')['Salary'].mean()
# Visualization
df.plot(x='Name', y='Salary', kind='bar')
plt.show()
Try these yourself:
# Install Flask: pip install flask
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
# Route decorators
@app.route('/')
def home():
return "Welcome to my Flask app!"
@app.route('/about')
def about():
return "About page"
# Dynamic routes
@app.route('/user/')
def user_profile(username):
return f"Profile page for {username}"
# Query parameters
@app.route('/search')
def search():
query = request.args.get('q', '')
return f"Searching for: {query}"
# HTML templates (create templates/index.html)
@app.route('/template')
def template_example():
data = {
'title': 'My Page',
'users': ['Alice', 'Bob', 'Charlie']
}
return render_template('index.html', **data)
# Forms - POST request
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# Validate credentials
return redirect(url_for('dashboard'))
return render_template('login.html')
@app.route('/dashboard')
def dashboard():
return "Welcome to dashboard!"
# Run the app
if __name__ == '__main__':
app.run(debug=True)
# Access at: http://localhost:5000/
Try these yourself:
# Install: pip install flask-sqlalchemy
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# Define models
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'created_at': self.created_at.isoformat()
}
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('User', backref='posts')
# Create tables
with app.app_context():
db.create_all()
# CRUD Operations
# Create
@app.route('/users', methods=['POST'])
def create_user():
data = request.get_json()
user = User(username=data['username'], email=data['email'])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
# Read
@app.route('/users', methods=['GET'])
def get_users():
users = User.query.all()
return jsonify([u.to_dict() for u in users])
@app.route('/users/', methods=['GET'])
def get_user(user_id):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
# Update
@app.route('/users/', methods=['PUT'])
def update_user(user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
user.username = data.get('username', user.username)
user.email = data.get('email', user.email)
db.session.commit()
return jsonify(user.to_dict())
# Delete
@app.route('/users/', methods=['DELETE'])
def delete_user(user_id):
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return jsonify({"message": "User deleted"})
# Queries
# User.query.filter_by(username='Alice').first()
# User.query.filter(User.email.like('%@gmail.com')).all()
# User.query.order_by(User.created_at.desc()).all()
Try these yourself:
# 1. Virtual Environment
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# 2. Requirements file
pip freeze > requirements.txt
# Install dependencies
pip install -r requirements.txt
# 3. Environment Variables
# .env file (don't commit!)
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=your-secret-key-here
DEBUG=False
# Load in Python
import os
from dotenv import load_dotenv
load_dotenv()
DATABASE_URL = os.getenv('DATABASE_URL')
# 4. Git Basics
git init
git add .
git commit -m "Initial commit"
git push origin main
# .gitignore
__pycache__/
*.pyc
venv/
.env
*.db
# 5. Deploy to Heroku
# Procfile
web: gunicorn app:app
# runtime.txt
python-3.11.0
# Deploy commands
heroku create myapp
git push heroku main
heroku open
# PEP 8 - Style Guide
# โ
Good naming
user_name = "Alice" # Variables: snake_case
MAX_CONNECTIONS = 100 # Constants: UPPER_CASE
class UserAccount: # Classes: PascalCase
pass
# โ
Clear function names
def calculate_total_price(items):
"""Calculate total price of items."""
return sum(item.price for item in items)
# โ
Type hints (Python 3.5+)
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age} years old"
# โ
List comprehensions
squares = [x**2 for x in range(10)]
# โ
Context managers
with open('file.txt', 'r') as f:
content = f.read()
# โ
Error handling
try:
result = risky_operation()
except ValueError as e:
logger.error(f"Error: {e}")
raise
finally:
cleanup()
# โ
Logging instead of print
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Processing started")
# โ
Documentation
def complex_function(param1, param2):
"""
Brief description of function.
Args:
param1 (str): Description of param1
param2 (int): Description of param2
Returns:
bool: True if successful, False otherwise
Raises:
ValueError: If param1 is empty
"""
pass
Build these to practice everything:
You've completed the Python Programming Course! ๐
You've learned: Variables โ Control Flow โ Functions โ OOP โ Design Patterns โ Testing โ APIs โ Databases โ Deployment
Next Steps: Build projects, contribute to open source, explore frameworks (Django, FastAPI), learn data science (NumPy, pandas, scikit-learn), or dive into automation and DevOps!
Test your Python knowledge with 20 random questions. Navigate one question at a time. Your progress is automatically saved!