🧠 Advanced Python Topics

Master Advanced Concepts, Libraries, and Real-World Applications

🎯 What You'll Learn

1. Decorators

Decorators are a powerful feature in Python that allow you to modify or enhance functions and classes without changing their source code.

💡 What are Decorators?

Decorators are functions that take another function as an argument and return a new function that usually extends the behavior of the original function.

Basic Decorator

# Simple decorator example 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

# Decorator that accepts 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(times=3) def greet(name): print(f"Hello, {name}!") greet("Alice") # Output: Hello, Alice! (printed 3 times)

Practical Decorators

# Timing decorator import time from functools import wraps def timer(func): @wraps(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(2) return "Done" slow_function() # Output: slow_function took 2.0001 seconds # Caching decorator (memoization) def memoize(func): cache = {} @wraps(func) def wrapper(*args): if args in cache: return cache[args] result = func(*args) cache[args] = result return result return wrapper @memoize def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(100)) # Fast with memoization!

2. Generators and Iterators

Generators are a simple way to create iterators. They allow you to generate values on-the-fly, saving memory.

💡 Why Use Generators?

Generators are memory-efficient because they generate values one at a time instead of storing them all in memory. Perfect for working with large datasets or infinite sequences.

Generator Functions

# 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 expression (like list comprehension) squares = (x**2 for x in range(10)) print(next(squares)) # 0 print(next(squares)) # 1

Practical Generator Examples

# Reading large files efficiently def read_large_file(file_path): with open(file_path, 'r') as file: for line in file: yield line.strip() # Process one line at a time without loading entire file for line in read_large_file('huge_file.txt'): process(line) # Infinite sequence generator def fibonacci_generator(): a, b = 0, 1 while True: yield a a, b = b, a + b # Get first 10 Fibonacci numbers fib = fibonacci_generator() for _ in range(10): print(next(fib))

Custom Iterators

# Creating a custom iterator class class Countdown: def __init__(self, start): self.current = start def __iter__(self): return self def __next__(self): if self.current <= 0: raise StopIteration self.current -= 1 return self.current + 1 for num in Countdown(5): print(num) # 5, 4, 3, 2, 1

3. Context Managers

Context managers allow you to allocate and release resources precisely when you want to. The most common use is with the with statement.

Built-in Context Managers

# File handling with context manager with open('file.txt', 'r') as file: content = file.read() print(content) # File is automatically closed here # Multiple context managers with open('input.txt', 'r') as infile, \ open('output.txt', 'w') as outfile: for line in infile: outfile.write(line.upper())

Creating Custom Context Managers

# Using contextlib decorator from contextlib import contextmanager @contextmanager def timer_context(name): import time start = time.time() print(f"Starting {name}...") try: yield finally: end = time.time() print(f"{name} completed in {end - start:.2f}s") with timer_context("Data Processing"): # Your code here import time time.sleep(1) # Class-based 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 = "Connected" # Actual DB connection here 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 # Don't suppress exceptions with DatabaseConnection("my_database") as conn: print(f"Using {conn}")

4. Advanced OOP

Dive deeper into object-oriented programming with metaclasses, abstract base classes, and advanced inheritance patterns.

Abstract Base Classes (ABC)

from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass @abstractmethod def perimeter(self): pass class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return 2 * (self.width + self.height) rect = Rectangle(5, 3) print(rect.area()) # 15 print(rect.perimeter()) # 16

Property Decorators

class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Temperature below absolute zero!") self._celsius = value @property def fahrenheit(self): return self._celsius * 9/5 + 32 @fahrenheit.setter def fahrenheit(self, value): self._celsius = (value - 32) * 5/9 temp = Temperature(25) print(temp.fahrenheit) # 77.0 temp.fahrenheit = 100 print(temp.celsius) # 37.78

Class and Static Methods

class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def from_string(cls, date_string): """Create Date from string 'YYYY-MM-DD'""" year, month, day = map(int, date_string.split('-')) return cls(year, month, day) @staticmethod def is_leap_year(year): """Check if year is leap year""" return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) # Using class method as alternative constructor date = Date.from_string("2024-03-15") # Using static method print(Date.is_leap_year(2024)) # True

5. Exception Handling

Proper exception handling makes your code robust and user-friendly.

Basic Exception Handling

# Try-except-else-finally try: result = 10 / 2 except ZeroDivisionError: print("Cannot divide by zero!") except TypeError as e: print(f"Type error: {e}") else: print(f"Result: {result}") # Runs if no exception finally: print("Cleanup code") # Always runs

Custom Exceptions

# Creating custom exceptions class InsufficientFundsError(Exception): def __init__(self, balance, amount): self.balance = balance self.amount = amount self.message = f"Insufficient funds: ${balance} < ${amount}" super().__init__(self.message) class BankAccount: def __init__(self, balance): self.balance = balance def withdraw(self, amount): if amount > self.balance: raise InsufficientFundsError(self.balance, amount) self.balance -= amount return self.balance account = BankAccount(100) try: account.withdraw(150) except InsufficientFundsError as e: print(e.message)

6. File I/O and Data Formats

Work with files and various data formats including JSON, CSV, and pickle.

Working with JSON

import json # Writing JSON data = { "name": "Alice", "age": 30, "skills": ["Python", "SQL", "ML"] } with open('data.json', 'w') as f: json.dump(data, f, indent=4) # Reading JSON with open('data.json', 'r') as f: loaded_data = json.load(f) print(loaded_data["name"]) # JSON string operations json_string = json.dumps(data, indent=2) parsed = json.loads(json_string)

Working with CSV

import csv # Writing CSV with open('employees.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['Name', 'Age', 'Department']) writer.writerow(['Alice', 30, 'IT']) writer.writerow(['Bob', 25, 'HR']) # Reading CSV with open('employees.csv', 'r') as f: reader = csv.reader(f) for row in reader: print(row) # Using DictReader/DictWriter with open('employees.csv', 'r') as f: reader = csv.DictReader(f) for row in reader: print(f"{row['Name']} works in {row['Department']}")

7. Regular Expressions

Regular expressions (regex) are powerful tools for pattern matching and text processing.

import re # Pattern matching text = "Contact us at support@example.com or sales@example.com" # Find all email addresses emails = re.findall(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', text) print(emails) # ['support@example.com', 'sales@example.com'] # Search and match pattern = r'(\d{3})-(\d{3})-(\d{4})' phone = "Call me at 555-123-4567" match = re.search(pattern, phone) if match: area, prefix, line = match.groups() print(f"Area: {area}, Prefix: {prefix}, Line: {line}") # Substitution text = "The price is $100" result = re.sub(r'\$(\d+)', r'€\1', text) print(result) # "The price is €100" # Validation def is_valid_email(email): pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None print(is_valid_email("user@example.com")) # True print(is_valid_email("invalid.email")) # False

8. Modules and Packages

Organize your code into reusable modules and packages.

📦 Module vs Package

Module: A single .py file
Package: A directory containing multiple modules and an __init__.py file

Creating and Using Modules

# mymodule.py def greet(name): return f"Hello, {name}!" def add(a, b): return a + b PI = 3.14159 # main.py import mymodule print(mymodule.greet("Alice")) print(mymodule.add(5, 3)) print(mymodule.PI) # Alternative imports from mymodule import greet, PI from mymodule import * # Import everything (not recommended) import mymodule as mm # Alias

Creating a Package

# Package structure: # mypackage/ # __init__.py # module1.py # module2.py # subpackage/ # __init__.py # module3.py # mypackage/__init__.py from .module1 import func1 from .module2 import func2 __version__ = "1.0.0" __all__ = ["func1", "func2"] # Using the package import mypackage print(mypackage.__version__) mypackage.func1() from mypackage.subpackage import module3

9. GUI Development with Tkinter

Create desktop applications with graphical user interfaces using Python's built-in Tkinter library.

Basic Tkinter Application

import tkinter as tk from tkinter import messagebox def say_hello(): name = entry.get() messagebox.showinfo("Greeting", f"Hello, {name}!") # Create main window root = tk.Tk() root.title("Hello App") root.geometry("300x150") # Widgets label = tk.Label(root, text="Enter your name:", font=("Arial", 12)) label.pack(pady=10) entry = tk.Entry(root, width=30) entry.pack(pady=5) button = tk.Button(root, text="Greet", command=say_hello, bg="#4CAF50", fg="white") button.pack(pady=10) root.mainloop()

More Complex GUI Example

import tkinter as tk from tkinter import ttk class CalculatorApp: def __init__(self, root): self.root = root root.title("Simple Calculator") # Entry field self.result_var = tk.StringVar() entry = tk.Entry(root, textvariable=self.result_var, font=("Arial", 18)) entry.grid(row=0, column=0, columnspan=4, sticky="nsew", padx=5, pady=5) # Button grid buttons = [ ['7', '8', '9', '/'], ['4', '5', '6', '*'], ['1', '2', '3', '-'], ['0', '.', '=', '+'] ] for i, row in enumerate(buttons, start=1): for j, btn_text in enumerate(row): btn = tk.Button(root, text=btn_text, width=5, height=2, command=lambda x=btn_text: self.on_button_click(x)) btn.grid(row=i, column=j, sticky="nsew") def on_button_click(self, value): if value == '=': try: result = eval(self.result_var.get()) self.result_var.set(result) except: self.result_var.set("Error") else: self.result_var.set(self.result_var.get() + value) root = tk.Tk() app = CalculatorApp(root) root.mainloop()

10. Database Operations

Work with databases using Python's built-in SQLite and SQLAlchemy ORM.

SQLite Basics

import sqlite3 # Create connection and cursor conn = sqlite3.connect('database.db') cursor = conn.cursor() # Create table cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER ) ''') # Insert data cursor.execute("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", ("Alice", "alice@example.com", 30)) # Insert multiple rows users = [ ("Bob", "bob@example.com", 25), ("Charlie", "charlie@example.com", 35) ] cursor.executemany("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", users) # Query data cursor.execute("SELECT * FROM users WHERE age > ?", (25,)) results = cursor.fetchall() for row in results: print(row) # Update data cursor.execute("UPDATE users SET age = ? WHERE name = ?", (31, "Alice")) # Delete data cursor.execute("DELETE FROM users WHERE age < ?", (18,)) # Commit and close conn.commit() conn.close()

Using Context Manager with SQLite

import sqlite3 with sqlite3.connect('database.db') as conn: cursor = conn.cursor() # Create and populate table cursor.execute(''' CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY, name TEXT, price REAL ) ''') cursor.execute("INSERT INTO products (name, price) VALUES (?, ?)", ("Laptop", 999.99)) # Query with row_factory for dictionary access conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM products") for row in cursor.fetchall(): print(f"{row['name']}: ${row['price']}") # Connection auto-commits and closes

11. Web Scraping

Extract data from websites using BeautifulSoup and requests.

Basic Web Scraping

import requests from bs4 import BeautifulSoup # Fetch webpage url = "https://example.com" response = requests.get(url) if response.status_code == 200: # Parse HTML soup = BeautifulSoup(response.content, 'html.parser') # Find elements title = soup.find('title').text print(f"Page title: {title}") # Find all links links = soup.find_all('a') for link in links: href = link.get('href') text = link.text print(f"{text}: {href}") # Find by class articles = soup.find_all('div', class_='article') # Find by ID main_content = soup.find(id='content') # CSS selectors headers = soup.select('h1, h2, h3')

Advanced Scraping Example

import requests from bs4 import BeautifulSoup import csv def scrape_products(url): response = requests.get(url) soup = BeautifulSoup(response.content, 'html.parser') products = [] for item in soup.find_all('div', class_='product'): name = item.find('h3').text.strip() price = item.find('span', class_='price').text.strip() rating = item.find('div', class_='rating').text.strip() products.append({ 'name': name, 'price': price, 'rating': rating }) return products # Save to CSV products = scrape_products("https://example.com/products") with open('products.csv', 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=['name', 'price', 'rating']) writer.writeheader() writer.writerows(products)

⚠️ Web Scraping Ethics

  • Always check robots.txt
  • Respect rate limits and add delays
  • Check website's Terms of Service
  • Don't overload servers with requests
  • Consider using official APIs when available

12. NumPy for Numerical Computing

NumPy is the foundation for numerical computing in Python, providing powerful array operations.

NumPy Arrays

import numpy as np # Creating arrays arr1 = np.array([1, 2, 3, 4, 5]) arr2 = np.array([[1, 2, 3], [4, 5, 6]]) # Special arrays zeros = np.zeros((3, 3)) ones = np.ones((2, 4)) identity = np.eye(3) random = np.random.rand(3, 3) # Random values 0-1 arange = np.arange(0, 10, 2) # [0, 2, 4, 6, 8] linspace = np.linspace(0, 1, 5) # 5 evenly spaced values # Array operations arr = np.array([1, 2, 3, 4]) print(arr * 2) # [2, 4, 6, 8] print(arr + 10) # [11, 12, 13, 14] print(arr ** 2) # [1, 4, 9, 16] # Statistical operations print(arr.mean()) # 2.5 print(arr.std()) # Standard deviation print(arr.sum()) # 10 print(arr.min()) # 1 print(arr.max()) # 4

Array Indexing and Slicing

import numpy as np arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) print(arr[0, 1]) # 2 (row 0, col 1) print(arr[0]) # [1, 2, 3] (first row) print(arr[:, 1]) # [2, 5, 8] (second column) print(arr[:2, 1:]) # [[2, 3], [5, 6]] # Boolean indexing print(arr[arr > 5]) # [6, 7, 8, 9] # Reshaping reshaped = arr.reshape(1, 9) # 1x9 array flattened = arr.flatten() # 1D array

13. Data Visualization with Matplotlib

Create stunning visualizations with Matplotlib, Python's most popular plotting library.

Basic Plotting

import matplotlib.pyplot as plt import numpy as np # Line plot x = np.linspace(0, 10, 100) y = np.sin(x) plt.figure(figsize=(10, 6)) plt.plot(x, y, label='sin(x)', color='blue', linewidth=2) plt.title('Sine Wave', fontsize=16) plt.xlabel('x') plt.ylabel('sin(x)') plt.grid(True, alpha=0.3) plt.legend() plt.show() # Multiple plots plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.plot(x, np.sin(x)) plt.title('Sine') plt.subplot(1, 2, 2) plt.plot(x, np.cos(x), color='red') plt.title('Cosine') plt.tight_layout() plt.show()

Different Plot Types

import matplotlib.pyplot as plt import numpy as np # Bar chart categories = ['A', 'B', 'C', 'D'] values = [25, 40, 30, 55] plt.bar(categories, values, color='skyblue') plt.title('Bar Chart Example') plt.show() # Scatter plot x = np.random.randn(100) y = np.random.randn(100) colors = np.random.rand(100) plt.scatter(x, y, c=colors, s=50, alpha=0.6, cmap='viridis') plt.colorbar() plt.title('Scatter Plot') plt.show() # Histogram data = np.random.randn(1000) plt.hist(data, bins=30, edgecolor='black', alpha=0.7) plt.title('Histogram') plt.xlabel('Value') plt.ylabel('Frequency') plt.show() # Pie chart sizes = [30, 25, 20, 25] labels = ['A', 'B', 'C', 'D'] plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90) plt.title('Pie Chart') plt.show()

14. Concurrency and Parallelism

Speed up your code with threading and multiprocessing.

Threading

import threading import time def worker(name, delay): print(f"{name} starting") time.sleep(delay) print(f"{name} finished") # Create threads threads = [] for i in range(5): t = threading.Thread(target=worker, args=(f"Thread-{i}", 1)) threads.append(t) t.start() # Wait for all threads for t in threads: t.join() print("All threads completed")

Multiprocessing

from multiprocessing import Pool import time def square(n): time.sleep(1) return n * n if __name__ == '__main__': # Create process pool with Pool(processes=4) as pool: numbers = range(10) results = pool.map(square, numbers) print(results)

💡 Threading vs Multiprocessing

Threading: Good for I/O-bound tasks (network, file operations)
Multiprocessing: Good for CPU-bound tasks (calculations, data processing)

15. Resources and Next Steps

📚 Recommended Books

Advanced Python

  • Fluent Python by Luciano Ramalho
  • Effective Python by Brett Slatkin
  • Python Cookbook by David Beazley

Specialized Topics

  • Web Scraping with Python by Ryan Mitchell
  • Python for Data Analysis by Wes McKinney
  • Automate the Boring Stuff with Python

🔗 Next Steps