Python Advanced From Intermediate to Pro – Master Modern Python in 2026
If you're seeing this book's cover or link pointing to Amazon.com (USA marketplace)
Table of Contents
Advanced Python Mastery – From Intermediate to Pro in 2026 Complete Notes for Serious Learners & Developers
Python Intermediate Recap & Advanced Setup 1.1 Quick Review: Lists, Dicts, Functions, Modules 1.2 Virtual Environments & pip (venv, requirements.txt) 1.3 Code Formatting & Linting (Black, Flake8, isort) 1.4 Type Hints & Static Typing (typing module, mypy) 1.5 Debugging Techniques (pdb, logging, VS Code debugger)
Object-Oriented Programming (OOP) in Depth 2.1 Classes & Objects – Advanced Features 2.2 init, self, str, repr 2.3 Inheritance & super() 2.4 Method Overriding & Polymorphism 2.5 Encapsulation: Private & Protected Members 2.6 Properties (@property, @setter, @deleter) 2.7 Class Methods, Static Methods, @classmethod, @staticmethod 2.8 Multiple Inheritance & Method Resolution Order (MRO) 2.9 Abstract Base Classes (abc module) 2.10 Composition vs Inheritance – When to Use What
Advanced Data Structures & Collections 3.1 collections module: namedtuple, deque, Counter, defaultdict, OrderedDict 3.2 dataclasses (Python 3.7+) – Cleaner Classes 3.3 Heapq – Priority Queues 3.4 Bisect – Binary Search & Insertion
Functional Programming Tools 4.1 Lambda Functions – Advanced Uses 4.2 map(), filter(), reduce() (functools) 4.3 List, Dict & Set Comprehensions – Advanced Patterns 4.4 Generator Expressions vs List Comprehensions 4.5 Generators & yield – Memory Efficient Iteration 4.6 Generator Functions, Generator Expressions 4.7 yield from & Sub-generators 4.8 itertools module – Powerful Iterators
Decorators & Higher-Order Functions 5.1 What are Decorators? 5.2 Writing Simple Decorators 5.3 Decorators with Arguments 5.4 @property, @classmethod, @staticmethod as Decorators 5.5 @lru_cache (functools) – Memoization 5.6 Chaining Decorators 5.7 Class Decorators
Context Managers & with Statement 6.1 Understanding Context Managers 6.2 Writing Custom Context Managers (enter, exit) 6.3 @contextmanager Decorator 6.4 Common Use Cases: File Handling, Database Connections, Timing
Exception Handling – Advanced 7.1 try-except-else-finally Deep Dive 7.2 Raising Custom Exceptions 7.3 Creating Custom Exception Classes 7.4 Exception Chaining (cause, context) 7.5 Logging vs print() for Production
File Handling & Data Formats 8.1 Reading/Writing Text & Binary Files 8.2 with Statement Best Practices 8.3 CSV – csv module 8.4 JSON – json module & serialization 8.5 Pickle – Serializing Python Objects 8.6 Working with Large Files (chunk reading)
Concurrency & Parallelism 9.1 Threading vs Multiprocessing vs Asyncio 9.2 threading module – Basics & Locks 9.3 multiprocessing – Process Pools 9.4 asyncio – Async/Await Syntax 9.5 aiohttp, async file I/O 9.6 When to Use What (GIL Explanation)
Metaclasses & Advanced OOP 10.1 What are Metaclasses? 10.2 type() as a Metaclass 10.3 Creating Custom Metaclasses 10.4 new vs init 10.5 Practical Use Cases (Frameworks, ORMs)
Design Patterns in Python 11.1 Singleton, Factory, Abstract Factory 11.2 Observer, Strategy, Decorator Pattern 11.3 Pythonic Alternatives to Classic Patterns
Performance Optimization 12.1 Time & Space Complexity Basics 12.2 Profiling (cProfile, timeit) 12.3 Efficient Data Structures 12.4 Caching & Memoization 12.5 NumPy & Pandas for Speed
Testing in Python 13.1 unittest vs pytest 13.2 Writing Unit Tests 13.3 Mocking (unittest.mock) 13.4 Test-Driven Development (TDD) Basics
Popular Libraries & Real-World Tools 14.1 requests – HTTP & APIs 14.2 BeautifulSoup & Scrapy – Web Scraping 14.3 pandas & NumPy – Data Manipulation 14.4 Flask / FastAPI – Web Development 14.5 SQLAlchemy / Django ORM – Databases
Mini Advanced Projects & Best Practices 15.1 CLI Tool with argparse / click 15.2 Async Web Scraper 15.3 Custom Decorator-based Logger 15.4 Thread-Safe Counter Class 15.5 Data Pipeline with Generators 15.6 PEP 8, PEP 257, Documentation & Git Workflow
Next Level Roadmap (2026+) 16.1 Web Development (FastAPI, Django) 16.2 Data Science / Machine Learning 16.3 DevOps & Automation 16.4 Contributing to Open Source 16.5 Recommended Books & Courses
1. Python Intermediate Recap & Advanced Setup
Before diving into advanced Python, let’s quickly refresh the core concepts and set up a professional development environment (2026 standard).
1.1 Quick Review: Lists, Dicts, Functions, Modules
Lists (mutable, ordered)
Python
fruits = ["apple", "banana", "mango"] fruits.append("kiwi") # add fruits[1] = "cherry" # update print(fruits[::2]) # ['apple', 'mango']
Dictionaries (mutable, key-value)
Python
person = {"name": "Anshuman", "age": 25, "city": "Muzaffarpur"} person["skills"] = ["Python", "FastAPI"] print(person.get("phone", "Not found")) # Not found for k, v in person.items(): print(f"{k}: {v}")
Functions (with args, *kwargs, defaults, return)
Python
def greet(name="Guest", hobbies, *extra): msg = f"Hello {name}!" if hobbies: msg += f" Hobbies: {', '.join(hobbies)}" if extra: msg += f" | {extra}" return msg print(greet("Rahul", "coding", "cricket", city="Patna")) # Hello Rahul! Hobbies: coding, cricket | {'city': 'Patna'}
Modules (import styles)
Python
import math as m from datetime import datetime from random import randint, choice print(m.sqrt(16)) # 4.0 print(datetime.now().strftime("%d-%b-%Y")) # 05-Mar-2026 print(randint(1, 100))
Quick self-check question: What’s the difference between list.append() and list.extend()? (Answer: append adds one item, extend adds multiple from iterable)
1.2 Virtual Environments & pip (venv, requirements.txt)
Never install packages globally — always use virtual environments.
Create & activate venv (2026 best way):
Bash
# Create python -m venv venv # Windows venv\Scripts\activate # Mac/Linux source venv/bin/activate # You’ll see (venv) in terminal
Install packages
Bash
pip install requests pandas fastapi uvicorn
Create requirements.txt
Bash
pip freeze > requirements.txt
Install from requirements.txt (on new machine / sharing project)
Bash
pip install -r requirements.txt
Deactivate
Bash
deactivate
Pro Tip: Add venv/ to .gitignore so it’s not pushed to GitHub.
1.3 Code Formatting & Linting (Black, Flake8, isort)
Write beautiful, consistent code automatically.
Install the most popular tools (2026 standard stack):
Bash
pip install black flake8 isort mypy
Recommended VS Code settings (add to settings.json):
JSON
{ "editor.formatOnSave": true, "python.formatting.provider": "black", "isort.args": ["--profile", "black"], "python.linting.flake8Enabled": true, "python.linting.mypyEnabled": true }
Common commands
Bash
# Format all files black . # Sort imports isort . # Check style violations flake8 . # Fix auto-fixable issues (isort + black) isort . && black .
Before vs After (Black + isort)
Python
# Before def my_function(a,b,c=10,*args,**kwargs):pass # After def my_function(a, b, c=10, args, *kwargs): pass
1.4 Type Hints & Static Typing (typing module, mypy)
Add types to make code self-documenting and catch bugs early.
Basic type hints
Python
def add(a: int, b: int) -> int: return a + b def greet(name: str = "Guest") -> str: return f"Hello, {name}!" scores: list[int] = [85, 92, 78] person: dict[str, str | int] = {"name": "Anshuman", "age": 25}
Advanced hints (Python 3.9+)
Python
from typing import List, Dict, Optional, Union, Any def process_data(data: List[Dict[str, Union[int, str]]]) -> Optional[float]: # ... return None
Using mypy (static type checker)
Bash
mypy your_script.py # Or check whole project mypy .
Install mypy
Bash
pip install mypy
Benefits:
VS Code shows errors instantly
Reduces runtime bugs
Makes large projects maintainable
1.5 Debugging Techniques (pdb, logging, VS Code debugger)
1. print() debugging (quick but limited)
2. logging module (production standard)
Python
import logging logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s", filename="app.log" ) logging.debug("This is debug") logging.info("User logged in: Anshuman") logging.warning("Low disk space") logging.error("Failed to connect to API")
3. pdb – Built-in debugger
Python
import pdb def divide(x, y): pdb.set_trace() # breakpoint return x / y divide(10, 0)
Common pdb commands:
n (next line)
s (step into function)
c (continue)
p variable (print value)
q (quit)
4. VS Code Debugger (most recommended in 2026)
Open your .py file
Click left sidebar → Run & Debug icon
Click "create a launch.json file" → Python
Set breakpoints (click left of line number)
Press F5 or green play button
You can:
Step over/into/out
Watch variables
See call stack
Debug async code too
Mini Setup Checklist (run once per project):
python -m venv venv source venv/bin/activate # or venv\Scripts\activate on Windows pip install black flake8 isort mypy requests pandas fastapi uvicorn
This completes the full Python Intermediate Recap & Advanced Setup section — now you're ready for real advanced topics!
2. Object-Oriented Programming (OOP) in Depth
OOP lets you model real-world entities as objects with data (attributes) and behavior (methods). Python’s OOP is flexible, powerful, and very “Pythonic”.
2.1 Classes & Objects – Advanced Features
Class → Blueprint Object → Instance of the class
Basic syntax (recap + advanced)
Python
class Person: # Class attribute (shared by all instances) species = "Homo sapiens" def init(self, name, age): # Instance attributes self.name = name self.age = age def introduce(self): return f"Hi, I'm {self.name}, {self.age} years old." # Creating objects (instances) p1 = Person("Anshuman", 25) p2 = Person("Rahul", 24) print(p1.introduce()) # Hi, I'm Anshuman, 25 years old. print(p1.species) # Homo sapiens print(p2.species) # Homo sapiens (shared)
Advanced features already in use:
self → reference to the current instance
Class attributes vs instance attributes
Methods are functions attached to the class
2.2 init, self, str, repr
Special (magic/dunder) methods:
init → constructor (called when object is created)
self → explicit reference to instance (Python passes it automatically)
str → human-readable string (used by print()) repr → unambiguous, developer-friendly string (used in REPL/debugger)
Python
class Book: def init(self, title, author, year): self.title = title self.author = author self.year = year def str(self): return f"{self.title} by {self.author} ({self.year})" def repr(self): return f"Book(title='{self.title}', author='{self.author}', year={self.year})" b = Book("Python Mastery", "Anshuman", 2026) print(b) # Python Mastery by Anshuman (2026) ← str print(repr(b)) # Book(title='Python Mastery', author='Anshuman', year=2026)
Tip: Always implement both — str for users, repr for debugging.
2.3 Inheritance & super()
Inheritance lets a child class reuse code from a parent class.
Python
class Employee: def init(self, name, salary): self.name = name self.salary = salary def work(self): return f"{self.name} is working." class Developer(Employee): def init(self, name, salary, language): # super() calls parent's init super().__init__(name, salary) self.language = language def work(self): return f"{self.name} is coding in {self.language}." dev = Developer("Anshuman", 120000, "Python") print(dev.work()) # Anshuman is coding in Python. print(dev.salary) # 120000 (inherited)
super() is better than hardcoding parent class name — supports multiple inheritance.
2.4 Method Overriding & Polymorphism
Overriding → Child redefines parent method Polymorphism → Same method name, different behavior
Python
class Animal: def speak(self): return "Some sound..." class Dog(Animal): def speak(self): return "Woof!" class Cat(Animal): def speak(self): return "Meow!" animals = [Dog(), Cat(), Animal()] for animal in animals: print(animal.speak()) # Woof! / Meow! / Some sound...
Duck typing in Python: "If it walks like a duck and quacks like a duck, it’s a duck."
2.5 Encapsulation: Private & Protected Members
Python uses naming conventions (no strict private):
singleunderscore → protected (by convention, don’t access directly)
__double_underscore → name mangling (pseudo-private)
Python
class BankAccount: def init(self, owner, balance=0): self.owner = owner self._balance = balance # protected self.__secret_pin = 1234 # name mangled def deposit(self, amount): if amount > 0: self._balance += amount def get_balance(self): return self._balance acc = BankAccount("Anshuman", 10000) print(acc._balance) # 10000 (works but discouraged) # print(acc.__secret_pin) # AttributeError print(acc._BankAccount__secret_pin) # 1234 (name mangled)
Best practice: Use _ for "don't touch this" and properties for controlled access.
2.6 Properties (@property, @setter, @deleter)
Make attributes look like variables but control access.
Python
class Circle: def init(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value < 0: raise ValueError("Radius cannot be negative") self._radius = value @property def area(self): return 3.14159 self._radius * 2 @area.deleter def area(self): print("Area cache cleared (if any)") c = Circle(5) print(c.radius) # 5 ← looks like attribute print(c.area) # 78.53975 ← computed property c.radius = 10 # setter called # c.radius = -3 # ValueError del c.area # deleter called
Benefits: Validation, computed values, lazy evaluation, clean API.
2.7 Class Methods, Static Methods, @classmethod, @staticmethod
@classmethod → receives class (cls) instead of instance (self)
@staticmethod → no self or cls — regular function in class namespace
Python
class Employee: raise_amount = 1.10 # class attribute def init(self, name, salary): self.name = name self.salary = salary @classmethod def set_raise_amount(cls, amount): cls.raise_amount = amount @staticmethod def is_workday(day): # Monday=0 ... Sunday=6 return day.weekday() < 5 def apply_raise(self): self.salary *= Employee.raise_amount Employee.set_raise_amount(1.15) # class method import datetime today = datetime.date(2026, 3, 5) # Thursday print(Employee.is_workday(today)) # True
Use @classmethod for alternative constructors, factory methods. Use @staticmethod for utility functions related to class.
2.8 Multiple Inheritance & Method Resolution Order (MRO)
Python supports multiple inheritance.
Python
class A: def show(self): print("A") class B(A): def show(self): print("B") class C(A): def show(self): print("C") class D(B, C): pass d = D() d.show() # B print(D.mro()) # [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, ...]
MRO (Method Resolution Order) → C3 linearization Use ClassName.mro() or ClassName.__mro__ to see order.
super() in multiple inheritance works beautifully with MRO.
2.9 Abstract Base Classes (abc module)
Force subclasses to implement methods.
Python
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass @abstractmethod def perimeter(self): pass class Rectangle(Shape): def init(self, length, width): self.length = length self.width = width def area(self): return self.length self.width def perimeter(self): return 2 (self.length + self.width) # s = Shape() # TypeError: Can't instantiate abstract class r = Rectangle(10, 5) print(r.area()) # 50
Use when: Defining interfaces or templates that must be followed.
2.10 Composition vs Inheritance – When to Use What
Inheritance ("is-a" relationship) → Dog is a Animal
Composition ("has-a" relationship) → Car has a Engine
Python
# Inheritance (is-a) class Car(Vehicle): ... # Composition (has-a) – usually preferred class Car: def init(self): self.engine = Engine() self.wheels = [Wheel() for _ in range(4)] def start(self): self.engine.start()
Rule of thumb (2026 best practice):
Prefer composition over inheritance (composition gives more flexibility)
Use inheritance only when there is a clear is-a relationship and you want to reuse behavior
"Favor composition over inheritance" – Gang of Four Design Patterns
Mini Project – Bank System (OOP in action)
Python
from abc import ABC, abstractmethod class Account(ABC): def init(self, account_number, balance=0): self.account_number = account_number self._balance = balance @property def balance(self): return self._balance @abstractmethod def withdraw(self, amount): pass def deposit(self, amount): if amount > 0: self._balance += amount return True return False class SavingsAccount(Account): INTEREST_RATE = 0.04 def withdraw(self, amount): if amount <= self._balance: self._balance -= amount return True return False def add_interest(self): self._balance += self._balance * SavingsAccount.INTEREST_RATE
This completes the full OOP in Depth section — now you can build clean, scalable, professional object-oriented systems!
3. Advanced Data Structures & Collections
Python’s standard library provides specialized data structures that go beyond basic list, dict, set, and tuple. These save time and improve performance.
3.1 collections module: namedtuple, deque, Counter, defaultdict, OrderedDict
The collections module offers high-performance alternatives.
1. namedtuple – Tuple with named fields (readable like objects, lightweight)
Python
from collections import namedtuple # Define Point = namedtuple('Point', ['x', 'y']) p1 = Point(10, 20) p2 = Point(x=5, y=15) # keyword args also work print(p1.x, p1.y) # 10 20 print(p1) # Point(x=10, y=20) print(p1._asdict()) # {'x': 10, 'y': 20}
Benefits: More readable than plain tuples, immutable, no memory overhead like classes.
2. deque – Double-ended queue (fast append/pop from both ends)
Python
from collections import deque dq = deque([1, 2, 3]) dq.append(4) # right dq.appendleft(0) # left print(dq) # deque([0, 1, 2, 3, 4]) print(dq.pop()) # 4 (from right) print(dq.popleft()) # 0 (from left) dq.extend([5, 6]) # right extend dq.extendleft([ -1, -2]) # left extend (reverses order!) print(dq) # deque([-2, -1, 1, 2, 3, 5, 6])
Use cases: Queues, stacks, sliding windows, BFS, undo/redo.
3. Counter – Counts hashable objects (like frequency map)
Python
from collections import Counter words = ["apple", "banana", "apple", "cherry", "banana", "apple"] cnt = Counter(words) print(cnt) # Counter({'apple': 3, 'banana': 2, 'cherry': 1}) print(cnt['apple']) # 3 print(cnt.most_common(2)) # [('apple', 3), ('banana', 2)] # Arithmetic operations c1 = Counter(a=3, b=1) c2 = Counter(a=1, b=2) print(c1 + c2) # Counter({'a': 4, 'b': 3}) print(c1 - c2) # Counter({'a': 2})
Use cases: Word frequency, voting systems, finding duplicates/most common items.
4. defaultdict – Dictionary that provides default value for missing keys
Python
from collections import defaultdict # Normal dict → KeyError on missing key d = {} # d['key'] += 1 # Error # defaultdict dd = defaultdict(int) # default = 0 dd['a'] += 1 dd['b'] += 5 print(dd) # defaultdict(<class 'int'>, {'a': 1, 'b': 5}) # Other defaults dd_list = defaultdict(list) dd_list['fruits'].append("apple") dd_list['fruits'].append("banana") print(dd_list['fruits']) # ['apple', 'banana']
Use cases: Grouping, counting without checking if key in dict.
5. OrderedDict – Dictionary that remembers insertion order (Note: Since Python 3.7, regular dict also preserves order — OrderedDict is mostly for explicit clarity or older code.)
Python
from collections import OrderedDict od = OrderedDict() od['a'] = 1 od['b'] = 2 od['c'] = 3 print(od) # OrderedDict([('a', 1), ('b', 2), ('c', 3)])
When to use OrderedDict today: When you need .popitem(last=False) (FIFO behavior) or want to clearly signal order matters.
3.2 dataclasses (Python 3.7+) – Cleaner Classes
dataclasses reduce boilerplate when creating classes mainly for storing data.
Python
from dataclasses import dataclass, field @dataclass class Person: name: str age: int = 0 city: str = "Unknown" hobbies: list[str] = field(default_factory=list) # mutable default safe def introduce(self): return f"Hi, I'm {self.name} from {self.city}, {self.age} years old." p = Person("Anshuman", 25, "Muzaffarpur") print(p) # Person(name='Anshuman', age=25, city='Muzaffarpur', hobbies=[]) p.hobbies.append("coding") print(p.hobbies) # ['coding'] p2 = Person("Rahul") # age=0, city="Unknown" print(p2) # Person(name='Rahul', age=0, city='Unknown', hobbies=[])
Advantages over regular class:
Auto init, repr, eq, ne
No need to write init manually
Safe mutable defaults with field(default_factory=...)
Type hints are enforced by tools like mypy
Options: @dataclass(frozen=True) → immutable, @dataclass(order=True) → adds comparison methods.
3.3 Heapq – Priority Queues
heapq provides min-heap (smallest element first) — very efficient for priority queues.
Python
import heapq # List as heap (in-place) tasks = [] # Push (priority, task) heapq.heappush(tasks, (3, "Write report")) heapq.heappush(tasks, (1, "Fix bug")) heapq.heappush(tasks, (2, "Review code")) print(heapq.heappop(tasks)) # (1, 'Fix bug') ← smallest priority first # Peek without pop print(tasks[0]) # (2, 'Review code')
Real example – Task scheduler
Python
import heapq from datetime import datetime queue = [] heapq.heappush(queue, (datetime(2026, 3, 10), "Submit project")) heapq.heappush(queue, (datetime(2026, 3, 6), "Exam revision")) while queue: deadline, task = heapq.heappop(queue) print(f"{deadline.date()}: {task}")
Tip: Use tuples (priority, item) — heap compares first element, then second if tie.
3.4 Bisect – Binary Search & Insertion
bisect maintains sorted lists efficiently (O(log n) search/insert).
Python
import bisect sorted_list = [10, 20, 30, 40, 50] # Find insertion point pos = bisect.bisect_left(sorted_list, 25) print(pos) # 2 # Insert while keeping sorted bisect.insort(sorted_list, 25) print(sorted_list) # [10, 20, 25, 30, 40, 50] # Right insertion (for duplicates) bisect.insort_right(sorted_list, 25) print(sorted_list) # [10, 20, 25, 25, 30, 40, 50]
Use cases: Maintaining sorted data, finding rank/position, interval problems.
Mini Project – Leaderboard with heapq & bisect
Python
import heapq # Min-heap for top 5 scores (negative for max-heap effect) leaderboard = [] def add_score(name, score): heapq.heappush(leaderboard, (-score, name)) # negative → largest first if len(leaderboard) > 5: heapq.heappop(leaderboard) # remove lowest add_score("Anshuman", 950) add_score("Rahul", 880) add_score("Priya", 990) # Show top scores print("Top 5:") for score, name in sorted(leaderboard): # sort for display print(f"{name}: {-score}")
This completes the full Advanced Data Structures & Collections section — now you can write more efficient, Pythonic code!
4. Functional Programming Tools
4.1 Lambda Functions – Advanced Uses
Lambda functions are anonymous (nameless) one-line functions — ideal for short operations passed to higher-order functions.
Basic syntax lambda arguments: expression
Advanced & practical uses
Sorting with custom key
Python
students = [ {"name": "Anshuman", "marks": 92}, {"name": "Rahul", "marks": 85}, {"name": "Priya", "marks": 98} ] # Sort by marks descending sorted_students = sorted(students, key=lambda x: x["marks"], reverse=True) print([s["name"] for s in sorted_students]) # ['Priya', 'Anshuman', 'Rahul']
Immediate function call (IIFE-like)
Python
result = (lambda x, y: x * y + 10)(5, 3) print(result) # 25
With map/filter in one-liners
Python
numbers = [1, 2, 3, 4, 5] squared_even = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers))) print(squared_even) # [4, 16]
As default value in dict (strategy pattern)
Python
operations = { "+": lambda x, y: x + y, "-": lambda x, y: x - y, "*": lambda x, y: x y } print(operations[""](10, 4)) # 40
Tip: Lambdas are great for short callbacks — but for complex logic (> 1–2 lines), use def for readability.
4.2 map(), filter(), reduce() (functools)
Higher-order functions that apply functions to iterables.
map(function, iterable, ...) → applies function to each item
Python
numbers = [1, 2, 3, 4] squares = map(lambda x: x**2, numbers) print(list(squares)) # [1, 4, 9, 16] # Multiple iterables names = ["anshuman", "rahul"] cities = ["Muzaffarpur", "Patna"] result = map(lambda n, c: f"{n.title()} from {c}", names, cities) print(list(result)) # ['Anshuman from Muzaffarpur', 'Rahul from Patna']
filter(function, iterable) → keeps items where function returns True
Python
numbers = [10, 15, 20, 25, 30] evens = filter(lambda x: x % 2 == 0, numbers) print(list(evens)) # [10, 20, 30]
reduce(function, iterable, initial) → from functools — reduces iterable to single value
Python
from functools import reduce numbers = [1, 2, 3, 4, 5] product = reduce(lambda x, y: x * y, numbers) print(product) # 120 # With initial value total_with_bonus = reduce(lambda acc, x: acc + x, numbers, 100) print(total_with_bonus) # 115
Modern note (2026): map & filter are still useful, but list comprehensions + reduce are more common. reduce is powerful for accumulations.
4.3 List, Dict & Set Comprehensions – Advanced Patterns
Comprehensions are Python’s most “functional” and readable way to create collections.
Advanced List Comprehension
Python
# Nested + condition matrix = [[1, 2], [3, 4], [5, 6]] flattened = [num for row in matrix for num in row if num % 2 == 0] print(flattened) # [2, 4, 6]
Dict Comprehension
Python
names = ["Anshuman", "Rahul", "Priya"] scores = [92, 85, 98] result = {name: score for name, score in zip(names, scores) if score >= 90} print(result) # {'Anshuman': 92, 'Priya': 98}
Set Comprehension
Python
words = ["apple", "banana", "cherry", "date"] unique_lengths = {len(word) for word in words} print(unique_lengths) # {5, 6, 4}
Conditional expression inside
Python
numbers = range(10) parity = ["even" if n % 2 == 0 else "odd" for n in numbers] print(parity[:5]) # ['even', 'odd', 'even', 'odd', 'even']
4.4 Generator Expressions vs List Comprehensions
List comprehension → creates full list in memory Generator expression → lazy, memory-efficient (yields one item at a time)
Python
# List comp → full list now squares_list = [x**2 for x in range(1000000)] # uses lots of memory # Generator expression → yields on demand squares_gen = (x**2 for x in range(1000000)) # almost no memory print(next(squares_gen)) # 0 print(next(squares_gen)) # 1
When to use generator expressions:
Large datasets
One-time iteration
Passing to functions like sum(), max(), list(), tuple()
Python
total = sum(x**2 for x in range(1000000)) # efficient! print(total)
4.5 Generators & yield – Memory Efficient Iteration
Generators are functions that use yield instead of return — they pause and resume execution.
Python
def countdown(n): while n > 0: yield n n -= 1 for num in countdown(5): print(num) # 5 4 3 2 1
Generator function vs normal function
Python
def normal(): return [1, 2, 3] # computes everything, returns list def gen(): yield 1 yield 2 yield 3 # pauses after each yield
4.6 Generator Functions, Generator Expressions
Generator expression (tuple-like syntax)
Python
gen_exp = (x**2 for x in range(10) if x % 2 == 0) print(list(gen_exp)) # [0, 4, 16, 36, 64]
Infinite generator example
Python
def infinite_fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b fib = infinite_fibonacci() print(next(fib)) # 0 print(next(fib)) # 1 print(next(fib)) # 1 print(next(fib)) # 2
4.7 yield from & Sub-generators
yield from delegates iteration to another iterable/generator — cleaner for chaining.
Python
def chain_generators(): yield from range(3) # 0,1,2 yield from "abc" # a,b,c yield from countdown(3) # 3,2,1 for val in chain_generators(): print(val) # 0 1 2 a b c 3 2 1
Flatten nested lists
Python
def flatten(nested): for sublist in nested: yield from sublist data = [[1,2], [3,4], [5]] print(list(flatten(data))) # [1, 2, 3, 4, 5]
4.8 itertools module – Powerful Iterators
itertools provides fast, memory-efficient iterators.
Common useful functions
count, cycle, repeat
Python
from itertools import count, cycle, repeat for num in count(10, 5): # 10, 15, 20, ... print(num) if num > 30: break for color in cycle(["red", "green", "blue"]): # repeats forever print(color) # break when needed
chain, zip_longest
Python
from itertools import chain, zip_longest print(list(chain([1,2], "abc", range(3)))) # [1, 2, 'a', 'b', 'c', 0, 1, 2] a = [1, 2] b = [10, 20, 30] print(list(zip_longest(a, b, fillvalue=0))) # [(1, 10), (2, 20), (0, 30)]
groupby (needs sorted input)
Python
from itertools import groupby from operator import itemgetter data = [("apple", 5), ("banana", 3), ("apple", 2), ("cherry", 8)] sorted_data = sorted(data, key=itemgetter(0)) for fruit, group in groupby(sorted_data, key=itemgetter(0)): print(fruit, list(group)) # apple [('apple', 5), ('apple', 2)] # banana [('banana', 3)] # cherry [('cherry', 8)]
Mini Project – Infinite Prime Generator with itertools
Python
from itertools import count, islice def is_prime(n): if n < 2: return False for i in range(2, int(n**0.5) + 1): if n % i == 0: return False return True primes = (n for n in count(2) if is_prime(n)) first_10_primes = list(islice(primes, 10)) print(first_10_primes) # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
This completes the full Functional Programming Tools section — now you can write concise, powerful, memory-efficient Python code!
5. Decorators & Higher-Order Functions
5.1 What are Decorators?
A decorator is a function that takes another function (or class) and returns a modified version of it — usually adding behavior before/after the original function runs.
Key points:
Decorators are higher-order functions (functions that take or return functions).
They use @ syntax for clean application.
Common uses: logging, timing, authentication, caching, validation.
Basic mental model:
Python
@decorator def my_function(): pass # is equivalent to: my_function = decorator(my_function)
5.2 Writing Simple Decorators
Step-by-step: Simple logging decorator
Python
def logger(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} returned: {result}") return result return wrapper @logger def add(a, b): return a + b print(add(5, 3))
Output:
text
Calling add with args=(5, 3), kwargs={} add returned: 8 8
Key parts:
wrapper is the inner function that adds behavior
args, *kwargs → accepts any arguments
Call original func and return its result
Preserve original function metadata (name, docstring, etc.)
Python
from functools import wraps def logger(func): @wraps(func) # Important! def wrapper(*args, **kwargs): print(f"→ {func.__name__} called") return func(*args, **kwargs) return wrapper
5.3 Decorators with Arguments
Sometimes you want to pass parameters to the decorator itself.
Example: Repeat decorator with count
Python
from functools import wraps def repeat(times): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for in range(times): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def sayhello(name): print(f"Hello, {name}!") say_hello("Anshuman")
Output:
text
Hello, Anshuman! Hello, Anshuman! Hello, Anshuman!
Structure: @repeat(3) → returns decorator → which returns wrapper
5.4 @property, @classmethod, @staticmethod as Decorators
These are built-in decorators that change how methods behave.
Python
class Circle: def init(self, radius): self._radius = radius @property # getter def radius(self): return self._radius @radius.setter # setter def radius(self, value): if value < 0: raise ValueError("Radius cannot be negative") self._radius = value @property def area(self): # computed property return 3.14159 self._radius * 2 @classmethod def from_diameter(cls, diameter): return cls(diameter / 2) # alternative constructor @staticmethod def is_valid_radius(r): return r >= 0 c = Circle(5) print(c.radius) # 5 ← looks like attribute print(c.area) # 78.53975 c.radius = 10 # setter works print(c.area) # 314.159 c2 = Circle.from_diameter(20) # class method print(c2.radius) # 10.0 print(Circle.is_valid_radius(-5)) # False ← static method
5.5 @lru_cache (functools) – Memoization
@lru_cache caches function results — huge performance boost for expensive recursive/ repeated calls.
Python
from functools import lru_cache @lru_cache(maxsize=128) # maxsize=None → unlimited def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(35)) # Very fast now! # Without cache: extremely slow
Use cases:
Recursive algorithms (Fibonacci, tree traversal)
API calls with same parameters
Expensive computations
Clear cache: fibonacci.cache_clear()
5.6 Chaining Decorators
You can stack multiple decorators — they apply from bottom to top.
Python
def uppercase(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() return wrapper def add_exclamation(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + "!" return wrapper @uppercase @add_exclamation def greet(name): return f"Hello {name}" print(greet("Anshuman")) # HELLO ANSHUMAN!
Order matters: @uppercase applied last → final output is uppercase.
5.7 Class Decorators
Decorators can also modify classes (less common but powerful).
Example: Add timestamp attribute to class
Python
from datetime import datetime from functools import wraps def add_timestamp(cls): original_init = cls.__init__ @wraps(original_init) def new_init(self, args, *kwargs): original_init(self, args, *kwargs) self.created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cls.__init__ = new_init return cls @add_timestamp class User: def init(self, name): self.name = name u = User("Anshuman") print(u.created_at) # e.g. 2026-03-05 16:45:22
Common real-world class decorators:
@dataclass
@singleton (ensure only one instance)
@register (auto-register classes in a factory)
Mini Project – Timing & Logging Decorator Combo
Python
import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() print(f"{func.__name__} took {end - start:.4f} seconds") return result return wrapper def logger(func): @wraps(func) def wrapper(*args, **kwargs): print(f"→ Running {func.__name__}...") result = func(*args, **kwargs) print(f"← {func.__name__} done.") return result return wrapper @timer @logger def slow_task(n): time.sleep(n) return f"Task completed after {n} seconds" print(slow_task(2))
Output:
text
→ Running slow_task... ← slow_task done. slow_task took 2.0012 seconds Task completed after 2 seconds
This completes the full Decorators & Higher-Order Functions section — now you can write elegant, reusable, and professional Python code using one of its most loved features!
6. Context Managers & with Statement
6.1 Understanding Context Managers
A context manager is an object that defines setup and teardown actions for a block of code. It is used with the with statement to handle resources like files, database connections, locks, etc.
Why use context managers?
Automatically close/release resources (even on exceptions)
Cleaner code (no manual try-finally)
Prevents resource leaks
Most common example – file handling
Python
# Old way (error-prone) f = open("data.txt", "w") try: f.write("Hello") finally: f.close() # Modern & safe way with open("data.txt", "w") as f: f.write("Hello") # auto-closed when block ends (even if error)
What happens behind the scenes:
open() returns a context manager object
enter() is called → returns the object (f)
Code inside with block runs
exit() is called → closes the file (even if exception occurs)
6.2 Writing Custom Context Managers (__enter__, exit)
You can create your own context manager by defining a class with enter and exit methods.
Example – Timing context manager
Python
import time class Timer: def enter(self): self.start = time.perf_counter() return self # returned value assigned to 'as' variable def exit(self, exc_type, exc_value, traceback): self.end = time.perf_counter() self.elapsed = self.end - self.start print(f"Execution time: {self.elapsed:.4f} seconds") # Return True to suppress exception, False to let it propagate return False # Usage with Timer() as t: time.sleep(1.5) print("Doing some work...") # Output: # Doing some work... # Execution time: 1.5012 seconds
Another example – Temporary directory change
Python
import os from pathlib import Path class ChangeDirectory: def init(self, path): self.new_path = path self.old_path = os.getcwd() def enter(self): os.chdir(self.new_path) return self def exit(self, *args): os.chdir(self.old_path) with ChangeDirectory(Path.home() / "Downloads"): print(os.getcwd()) # now in Downloads print(os.getcwd()) # back to original
Tip: exit receives exception info (exc_type, exc_value, traceback). Return True to suppress exception, False (default) to re-raise it.
6.3 @contextmanager Decorator
Writing full classes can be verbose. The @contextmanager decorator from contextlib lets you write context managers using a generator function — much simpler.
Syntax
Python
from contextlib import contextmanager @contextmanager def my_context(): # setup code try: yield value # value assigned to 'as' variable finally: # teardown code (always runs)
Example – Temporary value change
Python
from contextlib import contextmanager @contextmanager def temp_setting(obj, attr, new_value): old_value = getattr(obj, attr) setattr(obj, attr, new_value) try: yield finally: setattr(obj, attr, old_value) class Config: debug = False cfg = Config() with temp_setting(cfg, "debug", True): print(cfg.debug) # True print(cfg.debug) # False (restored)
Real-world example – Database transaction
Python
@contextmanager def transaction(db): db.begin() try: yield db db.commit() except Exception: db.rollback() raise finally: db.close() # Usage with transaction(my_db) as db: db.execute("INSERT INTO users ...")
6.4 Common Use Cases: File Handling, Database Connections, Timing
1. File Handling (built-in)
Python
with open("log.txt", "a") as f, open("error.txt", "a") as err: f.write("Info log\n") err.write("Error occurred\n") # Both files auto-closed
2. Database Connections (using contextlib + library)
Python
from contextlib import contextmanager import sqlite3 @contextmanager def sqlite_conn(db_path): conn = sqlite3.connect(db_path) try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() with sqlite_conn("mydb.db") as conn: conn.execute("INSERT INTO users (name) VALUES (?)", ("Anshuman",))
3. Timing / Performance Measurement
Python
from contextlib import contextmanager import time @contextmanager def timer(description=""): start = time.perf_counter() yield elapsed = time.perf_counter() - start print(f"{description} took {elapsed:.4f} seconds") with timer("Data processing"): time.sleep(1.2) # heavy computation here # Output: Data processing took 1.2008 seconds
4. Thread Lock / Resource Management
Python
from threading import Lock from contextlib import contextmanager lock = Lock() @contextmanager def locked(): lock.acquire() try: yield finally: lock.release() with locked(): # critical section print("Thread-safe code here")
Mini Project – Suppressing Specific Exceptions
Python
from contextlib import contextmanager, suppress @contextmanager def ignore_errors(*exceptions): try: yield except exceptions: pass with ignore_errors(ZeroDivisionError, ValueError): print(10 / 0) # no crash print("This won't run") print("Program continues...")
This completes the full Context Managers & with Statement section — now you can manage resources safely and elegantly in any Python program!
7. Exception Handling – Advanced
7.1 try-except-else-finally Deep Dive
The full structure of exception handling in Python:
Python
try: # Code that might raise an exception risky_operation() except ExceptionType1 as e1: # Handle specific exception type 1 print(f"Type1 error: {e1}") except (Type2, Type3) as e23: # Handle multiple types print(f"Type2/3 error: {e23}") except Exception as e: # Catch-all (broad, use carefully) print(f"Unexpected error: {e}") # Optionally re-raise: raise else: # Runs only if NO exception occurred print("Success! No exceptions raised.") finally: # Always runs (cleanup), even on return/break/raise print("Cleanup: closing files, connections, etc.")
Key points to remember:
else → only executes if no exception was raised in try (great for code that should run only on success)
finally → always executes (cleanup, close files/connections, release locks)
Order matters: except blocks are checked from top to bottom → specific → general
Avoid bare except: (catches everything, including KeyboardInterrupt, SystemExit)
Real example – File processing with proper cleanup
Python
def process_file(filename): try: f = open(filename, "r") data = f.read() number = int(data.strip()) # might raise ValueError except FileNotFoundError: print("File not found!") return None except ValueError as ve: print(f"Invalid number format: {ve}") return None else: print("File read successfully!") return number * 2 finally: if 'f' in locals(): f.close() print("File closed in finally.") print(process_file("numbers.txt"))
7.2 Raising Custom Exceptions
Use raise to signal errors explicitly.
Basic raise
Python
if age < 0: raise ValueError("Age cannot be negative!")
Raise with custom message
Python
def divide(a, b): if b == 0: raise ZeroDivisionError("Cannot divide by zero – check your input!") return a / b
Re-raise (preserve original traceback)
Python
try: risky_code() except Exception as e: print("Logging error...") raise # re-raises the original exception with full traceback
Chaining with raise ... from (see 7.4)
7.3 Creating Custom Exception Classes
Custom exceptions make error handling clearer and more semantic.
Basic custom exception
Python
class InvalidAgeError(ValueError): """Raised when age is invalid (negative or unrealistic).""" pass def set_age(age): if age < 0: raise InvalidAgeError("Age cannot be negative") if age > 150: raise InvalidAgeError("Age seems unrealistic") print(f"Age set to {age}")
Advanced custom exception with attributes
Python
class PaymentFailedError(Exception): def init(self, amount, reason, transaction_id=None): self.amount = amount self.reason = reason self.transaction_id = transaction_id super().__init__(f"Payment of ₹{amount} failed: {reason}") def str(self): msg = f"Payment failed: {self.reason} (₹{self.amount})" if self.transaction_id: msg += f" - Transaction ID: {self.transaction_id}" return msg try: # Simulate payment gateway failure raise PaymentFailedError(5000, "Card declined", "TXN987654") except PaymentFailedError as e: print(e) # Payment failed: Card declined (₹5000) - Transaction ID: TXN987654 print(f"Amount lost: ₹{e.amount}")
Best practice:
Inherit from built-in exceptions (ValueError, TypeError, RuntimeError, etc.)
Add custom attributes for debugging/info
Write docstrings for clarity
7.4 Exception Chaining (__cause__, context)
Python automatically tracks exception chains:
context → the exception that was being handled when this one was raised
cause → explicit cause (set with raise ... from)
Automatic chaining (implicit)
Python
try: 1 / 0 except ZeroDivisionError: raise ValueError("Division failed") # ValueError.__context__ = ZeroDivisionError
Explicit chaining with from
Python
def load_config(): try: with open("config.json") as f: return json.load(f) except FileNotFoundError as fnf: raise RuntimeError("Configuration loading failed") from fnf try: load_config() except RuntimeError as re: print(re) # Configuration loading failed print(re.__cause__) # shows FileNotFoundError
Use from None to suppress context
Python
raise ValueError("Bad value") from None # hides previous context
When to use:
from → when new exception is a direct consequence (cleaner traceback)
Automatic context → when handling one error leads to another
7.5 Logging vs print() for Production
print() → good for debugging, bad for production
logging → standard for real applications
Basic logging setup (recommended)
Python
import logging import sys # Configure once at app startup logging.basicConfig( level=logging.DEBUG, # DEBUG / INFO / WARNING / ERROR / CRITICAL format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[ logging.FileHandler("app.log"), logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger(__name__) # name = current module name def process_payment(amount): try: if amount <= 0: raise ValueError("Amount must be positive") logger.info(f"Processing payment of ₹{amount}") # simulate success logger.debug("Payment gateway response: success") return True except ValueError as ve: logger.error("Payment validation failed", exc_info=True) # includes traceback return False except Exception as e: logger.critical("Unexpected error in payment", exc_info=True) raise
Levels quick reference:
debug → detailed info (disabled in production)
info → normal operation messages
warning → something unexpected but not fatal
error → handled error (operation failed)
critical → severe error (app may crash)
Advantages over print():
Levels (turn off debug in production)
File + console output
Timestamps, module names, tracebacks
Configurable via logging.config (dictConfig, fileConfig)
Thread-safe
Pro tip: Use logger.exception("Message") instead of logger.error(..., exc_info=True) — shorter and includes traceback automatically.
Mini Project – Robust File Processor with Logging & Custom Exception
Python
import logging from contextlib import contextmanager logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") class FileProcessingError(Exception): pass @contextmanager def safe_file_read(filename): try: with open(filename, "r") as f: yield f.read() except FileNotFoundError: raise FileProcessingError(f"File {filename} not found") except PermissionError: raise FileProcessingError(f"No permission to read {filename}") except Exception as e: raise FileProcessingError(f"Unexpected error reading {filename}: {e}") try: with safe_file_read("config.txt") as content: logging.info("File read successfully") print(content) except FileProcessingError as fpe: logging.error(str(fpe))
This completes the full Exception Handling – Advanced section — now you can build robust, production-ready Python applications that handle errors gracefully!
8. File Handling & Data Formats
8.1 Reading/Writing Text & Binary Files
Python provides the built-in open() function to work with files.
Modes you will use most often:
'r' → read text (default)
'w' → write text (overwrites if exists)
'a' → append text
'rb' → read binary
'wb' → write binary
'r+' → read + write (file must exist)
Best practice: Always use with statement (automatically closes file)
Text file examples
Python
# Reading entire file with open("notes.txt", "r", encoding="utf-8") as file: content = file.read() # → one big string print(content) # Reading line by line (memory efficient) with open("log.txt", "r", encoding="utf-8") as file: for line in file: print(line.strip()) # process each line # Writing text with open("output.txt", "w", encoding="utf-8") as file: file.write("Hello, Anshuman!\n") file.write("This is line 2.\n") # Appending with open("log.txt", "a", encoding="utf-8") as file: file.write(f"New entry at {datetime.now()}\n")
Binary file example (copy image/video)
Python
with open("photo.jpg", "rb") as src: data = src.read() with open("backup.jpg", "wb") as dest: dest.write(data)
Important flags:
encoding="utf-8" → almost always use for text files (handles Hindi, emojis, etc.)
newline="" → use when writing CSV on Windows to avoid extra blank lines
8.2 with Statement Best Practices
The with statement is the safest and cleanest way to handle files (and other resources).
Correct & safe
Python
with open("data.txt", "r", encoding="utf-8") as f: content = f.read() # file is automatically closed here – even if exception occurs
Multiple files in one with
Python
with open("input.txt", "r") as src, open("copy.txt", "w") as dest: dest.write(src.read())
Nested with (when needed)
Python
with open("config.json") as cfg: with open("backup.log", "a") as log: log.write("Config loaded successfully\n")
Never do this (risk of file not closing)
Python
f = open("file.txt") try: data = f.read() finally: f.close() # easy to forget finally
8.3 CSV – csv module
The csv module handles commas, quotes, delimiters, and newlines correctly — never use split(',') for real CSV.
Reading CSV
Python
import csv # Simple reader with open("students.csv", "r", encoding="utf-8") as f: reader = csv.reader(f) header = next(reader) # ['name', 'age', 'city'] for row in reader: print(row) # ['Anshuman', '25', 'Muzaffarpur'] # DictReader – most useful with open("students.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: print(row["name"], row["age"]) # Anshuman 25
Writing CSV
Python
import csv data = [ {"name": "Rahul", "age": 24, "city": "Patna"}, {"name": "Priya", "age": 23, "city": "Delhi"} ] with open("output.csv", "w", encoding="utf-8", newline="") as f: writer = csv.DictWriter(f, fieldnames=["name", "age", "city"]) writer.writeheader() # writes header row writer.writerows(data)
Tip: newline="" prevents extra blank lines on Windows.
8.4 JSON – json module & serialization
JSON is the most common format for APIs, config files, web data.
Reading JSON
Python
import json with open("config.json", "r", encoding="utf-8") as f: config = json.load(f) # directly gets dict/list print(config["api"]["key"]) # your-api-key-here
Writing JSON (pretty print)
Python
import json person = { "name": "Anshuman", "age": 25, "skills": ["Python", "FastAPI", "SQL"], "address": {"city": "Muzaffarpur", "state": "Bihar"}, "active": True } with open("person.json", "w", encoding="utf-8") as f: json.dump(person, f, indent=4, ensure_ascii=False) # indent=4 → beautiful formatting # ensure_ascii=False → allows Hindi/Unicode without escaping
String conversion (very common in APIs)
Python
json_string = json.dumps(person, indent=2, ensure_ascii=False) print(json_string) back_to_dict = json.loads(json_string)
8.5 Pickle – Serializing Python Objects
pickle can save almost any Python object (lists, dicts, classes, functions, models, etc.) — but only use it for trusted data.
Important warning: Never load pickle files from untrusted sources → security risk (can execute arbitrary code)
Basic usage
Python
import pickle data = { "model_weights": some_large_array, "training_history": [0.92, 0.85, 0.89], "timestamp": datetime.now() } # Save with open("model.pkl", "wb") as f: pickle.dump(data, f) # Load with open("model.pkl", "rb") as f: loaded = pickle.load(f)
Use cases:
Save ML models (scikit-learn, PyTorch state_dict)
Cache expensive computations
Save game state, user sessions internally
Safer alternatives for sharing data: JSON, CSV, Parquet, HDF5
8.6 Working with Large Files (chunk reading)
Never load huge files (GBs) into memory at once.
Line-by-line (best for text/CSV)
Python
with open("very_large_log.txt", "r", encoding="utf-8") as f: for line in f: # process each line if "ERROR" in line: print(line.strip())
Chunk reading (binary or text)
Python
def process_in_chunks(filename, chunk_size=1024*1024): # 1 MB chunks with open(filename, "rb") as f: while chunk := f.read(chunk_size): # process chunk (e.g., hash, search bytes, upload) print(f"Processed {len(chunk)} bytes") process_in_chunks("big_video.mp4")
Memory-efficient CSV processing
Python
import csv with open("million_rows.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) total = 0 for row in reader: total += float(row["sales"]) # no need to store all rows print(f"Total sales: ₹{total:,.2f}")
Mini Project – Simple Log Analyzer (JSON Lines + chunks)
Python
import json def analyze_logs(filename): error_count = 0 with open(filename, "r", encoding="utf-8") as f: for line in f: try: log = json.loads(line.strip()) if log.get("level") == "ERROR": error_count += 1 print(f"Error: {log['message']}") except json.JSONDecodeError: print("Skipping invalid JSON line") print(f"Total errors: {error_count}") analyze_logs("server_logs.jsonl")
This completes the full File Handling & Data Formats section — now you can confidently handle any kind of file, from small configs to massive logs and datasets!
9. Concurrency & Parallelism
9.1 Threading vs Multiprocessing vs Asyncio – Quick Comparison
ApproachUses threads?Uses processes?Best forGIL effectOverheadTypical use casethreadingYesNoI/O-bound (waiting: network, files, DB)Limited (GIL blocks CPU)LowSimple background tasks, many file readsmultiprocessingNoYesCPU-bound (heavy math, image processing)No GIL per processHigh (memory)Parallel computation, ML trainingasyncioNo (co-op)NoHigh-volume I/O (thousands of requests)No issueVery lowWeb servers, API clients, scraping
GIL (Global Interpreter Lock) short explanation: CPython has one GIL → only one thread can execute Python bytecode at a time. → Threads are great for waiting (I/O), but almost no speedup for CPU work. → Multiprocessing creates separate processes → each has its own GIL → true multi-core usage.
2026 decision guide:
Need to wait for 100+ network calls? → asyncio (best) or threading
Need to run heavy math on all CPU cores? → multiprocessing
Simple script with few background tasks? → threading
Modern web/API/scraping? → asyncio
9.2 threading module – Basics & Locks
threading is good when your bottleneck is waiting (network, disk, database), not CPU.
Basic example – concurrent downloads (simulated)
Python
import threading import time def download_file(file_id, delay): print(f"Starting download {file_id}") time.sleep(delay) # simulate network delay print(f"Finished download {file_id}") threads = [] for i in range(1, 6): t = threading.Thread(target=download_file, args=(i, 2.0)) threads.append(t) t.start() for t in threads: t.join() # wait for all threads to finish print("All downloads completed")
Race condition problem (shared variable)
Python
counter = 0 def increment(): global counter for in range(100000): temp = counter time.sleep(0.000001) # simulate tiny delay counter = temp + 1 threads = [threading.Thread(target=increment) for in range(10)] for t in threads: t.start() for t in threads: t.join() print(counter) # Expected 1000_000, but usually much less!
Solution: Lock (Mutex)
Python
counter = 0 lock = threading.Lock() def safe_increment(): global counter for in range(100000): with lock: # automatically acquire & release counter += 1 # Now counter == 1_000_000 every time
Other tools in threading:
threading.RLock → reentrant lock (same thread can acquire multiple times)
threading.Event, threading.Condition, threading.Semaphore
queue.Queue → thread-safe queue (very useful)
Modern recommendation: Use concurrent.futures.ThreadPoolExecutor for simple thread pools.
9.3 multiprocessing – Process Pools
multiprocessing runs real OS processes → bypasses GIL → true parallelism on multiple CPU cores.
Important: Always protect main code with if name == "__main__": (especially on Windows)
Basic Process example
Python
from multiprocessing import Process def square(n): print(f"{n}² = {n*n}") if name == "__main__": processes = [Process(target=square, args=(i,)) for i in range(10)] for p in processes: p.start() for p in processes: p.join()
Best & easiest way: ProcessPoolExecutor
Python
from concurrent.futures import ProcessPoolExecutor import time def heavy_math(n): time.sleep(1) # simulate CPU work return n * n if name == "__main__": with ProcessPoolExecutor(max_workers=4) as executor: results = executor.map(heavy_math, range(10)) print(list(results)) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Shared data (be careful – slow & complex): Use multiprocessing.Value, Array, Manager, Queue, Pipe
Tip: For CPU-bound tasks → multiprocessing gives real speedup on multi-core machines.
9.4 asyncio – Async/Await Syntax
asyncio uses cooperative multitasking in a single thread — extremely efficient for I/O-heavy workloads.
Basic async example
Python
import asyncio async def fetch_data(source_id): print(f"Fetching from {source_id}...") await asyncio.sleep(1.5) # simulate network delay print(f"Done with {source_id}") return f"Data from {source_id}" async def main(): tasks = [fetch_data(i) for i in range(1, 6)] results = await asyncio.gather(*tasks) # run all concurrently print("All results:", results) if name == "__main__": asyncio.run(main()) # Python 3.7+ standard way
Output (finishes in ~1.5 seconds, not 7.5):
text
Fetching from 1... Fetching from 2... Fetching from 3... Fetching from 4... Fetching from 5... Done with 1 Done with 2 Done with 3 Done with 4 Done with 5 All results: ['Data from 1', 'Data from 2', 'Data from 3', 'Data from 4', 'Data from 5']
Key concepts:
async def → defines a coroutine
await → pauses coroutine until awaited task completes
asyncio.gather() → wait for multiple tasks concurrently
asyncio.create_task() → schedule without immediate await
9.5 aiohttp, async file I/O
aiohttp – asynchronous HTTP client/server (replaces requests for async code)
Install: pip install aiohttp
Concurrent HTTP requests
Python
import asyncio import aiohttp async def fetch(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.text() async def main(): urls = [ "https://python.org", "https://fastapi.tiangolo.com", "https://realpython.com" ] tasks = [fetch(url) for url in urls] results = await asyncio.gather(*tasks) print([len(html) for html in results]) # lengths of pages asyncio.run(main())
Async file reading/writing (aiofiles)
Python
# pip install aiofiles import aiofiles async def read_large_file(): async with aiofiles.open("big_log.txt", mode="r", encoding="utf-8") as f: async for line in f: # process line if "ERROR" in line: print(line.strip()) asyncio.run(read_large_file())
9.6 When to Use What (GIL Explanation)
GIL reminder: CPython has one Global Interpreter Lock → only one thread executes Python bytecode at a time. → I/O operations (network, disk) release GIL → threads work well → CPU-bound Python code keeps GIL → threads give almost no speedup
Practical 2026 decision table:
ScenarioRecommendedWhy / Alternative100+ API calls / web scrapingasyncio + aiohttpThousands concurrent, low CPU usageReading/writing many files or DB queriesasyncio + aiofilesNon-blocking I/OHeavy math, image processing, ML inferencemultiprocessingUses all CPU cores, bypasses GILSimple background tasks (email, logging)threadingEasy, low overheadWeb server (high connections)asyncio (FastAPI, aiohttp)Best scalabilityMixed I/O + CPU workasyncio + ProcessPoolExecutorAsync I/O + CPU offloaded to processes
Mini Project – Concurrent API Caller with asyncio
Python
import asyncio import aiohttp from aiohttp import ClientTimeout async def get_user(user_id): url = f"https://jsonplaceholder.typicode.com/users/{user_id}" timeout = ClientTimeout(total=5) try: async with aiohttp.ClientSession() as session: async with session.get(url, timeout=timeout) as resp: return await resp.json() except Exception as e: return {"error": str(e)} async def main(): tasks = [get_user(i) for i in range(1, 11)] results = await asyncio.gather(*tasks, return_exceptions=True) for user in results: if isinstance(user, dict) and "name" in user: print(f"{user['id']}: {user['name']}") else: print("Failed:", user) asyncio.run(main())
This completes the full Concurrency & Parallelism section — now you know how to choose and implement the right concurrency model for any task!
10. Metaclasses & Advanced OOP
10.1 What are Metaclasses?
In Python, everything is an object — including classes. Just like a class creates instances (objects), a metaclass creates classes.
Normal flow (you already know):
Python
class Person: pass p = Person() # Person is the class (blueprint), p is the instance
Metaclass flow:
A metaclass is responsible for creating the Person class itself.
The default metaclass in Python is type.
Python
print(type(Person)) # <class 'type'> print(type(int)) # <class 'type'> print(type(str)) # <class 'type'>
So type is the built-in metaclass that creates almost all classes.
When do you need custom metaclasses?
You want to automatically add methods/attributes to classes
Enforce coding standards (e.g., all methods must have docstrings)
Auto-register classes (used in ORMs like SQLAlchemy, Django models, plugins)
Change class creation behavior (very rare in everyday code)
10.2 type() as a Metaclass
type can be used in three ways:
As a function to check type: type(obj)
As a metaclass (default)
As a dynamic class factory
Dynamic class creation with type()
Python
# type(name, bases, dict) → creates a class dynamically def speak(self): return f"Hello, I'm {self.name}" Person = type( "Person", # class name (object,), # base classes { "name": "Anshuman", # class attribute "speak": speak, # method "age": 25 # another attribute } ) p = Person() print(p.speak()) # Hello, I'm Anshuman print(p.age) # 25
This is exactly how Python creates classes under the hood when you write class Person: ...
10.3 Creating Custom Metaclasses
To create a custom metaclass, you inherit from type and override special methods (usually new or init).
Most common pattern: Override new
Python
class MetaLogger(type): def new(cls, name, bases, attrs): # Add logging to every method automatically for attr_name, attr_value in attrs.items(): if callable(attr_value) and not attr_name.startswith("_"): def wrapper(*args, **kwargs): print(f"Calling {name}.{attr_name}()") result = attr_value(*args, **kwargs) print(f"{name}.{attr_name}() finished") return result attrs[attr_name] = wrapper return super().__new__(cls, name, bases, attrs) # Use the metaclass class MyService(metaclass=MetaLogger): def process_data(self, data): print(f"Processing: {data}") return len(data) s = MyService() s.process_data("Hello world")
Output:
text
Calling MyService.process_data() Processing: Hello world MyService.process_data() finished
10.4 new vs init
Both are special methods, but they serve different purposes:
MethodCalled onPurposeReturnsMost used in metaclasses?__new__Class creation (before instance)Creates & returns the object/class itselfThe new object/classYes (control class creation)__init__Instance creation (after new)Initializes the object (sets attributes)None (implicit)Rarely in metaclasses
Key difference in metaclasses:
Override new when you want to modify the class dictionary (methods, attributes) before the class is created.
Override init when you want to do something with the already-created class (less common).
Example: Enforce docstring on all methods
Python
class RequireDocstringMeta(type): def new(cls, name, bases, attrs): for attr_name, value in attrs.items(): if callable(value) and not attr_name.startswith("_"): if not value.__doc__: raise TypeError(f"Method {name}.{attr_name} must have a docstring!") return super().__new__(cls, name, bases, attrs) class MyClass(metaclass=RequireDocstringMeta): def good_method(self): """This has a docstring.""" pass # def bad_method(self): # This would raise TypeError # pass
10.5 Practical Use Cases (Frameworks, ORMs)
Metaclasses are rarely needed in everyday code — but they power some of the most famous Python libraries.
1. Django Models (ORM) Django uses metaclasses to turn a simple class into a full database model:
Python
class Book(models.Model): # metaclass=ModelBase title = models.CharField(max_length=200) author = models.ForeignKey(Author, on_delete=models.CASCADE)
→ Metaclass automatically adds manager (objects), table name, fields, etc.
2. SQLAlchemy Declarative Base
Python
class Base(DeclarativeBase): pass class User(Base): tablename = "users" id = Column(Integer, primary_key=True)
→ Metaclass inspects attributes and builds the mapping to database tables.
3. Plugin / Registry Systems Auto-register classes:
Python
class PluginRegistryMeta(type): registry = {} def new(cls, name, bases, attrs): new_cls = super().__new__(cls, name, bases, attrs) if name != "PluginBase": cls.registry[name] = new_cls return new_cls class PluginBase(metaclass=PluginRegistryMeta): pass class ImagePlugin(PluginBase): pass class VideoPlugin(PluginBase): pass print(PluginRegistryMeta.registry) # {'ImagePlugin': <class '__main__.ImagePlugin'>, 'VideoPlugin': <class '__main__.VideoPlugin'>}
4. Enforcing Design Patterns
Singleton pattern via metaclass
Auto-add logging, timing, validation to methods
ABC-like enforcement without abc module
Important advice (2026):
Use metaclasses only when you really need them — they make code harder to understand.
Prefer decorators, class decorators, or __init_subclass__ (Python 3.6+) for most cases.
If you're building a framework/library → metaclasses are powerful tools.
For application code → avoid unless there's no simpler solution.
Mini Project – Auto-Register Commands (CLI style)
Python
class CommandRegistryMeta(type): commands = {} def new(cls, name, bases, attrs): new_cls = super().__new__(cls, name, bases, attrs) if name != "Command": cls.commands[name.lower()] = new_cls return new_cls class Command(metaclass=CommandRegistryMeta): @classmethod def execute(cls): raise NotImplementedError class HelpCommand(Command): @classmethod def execute(cls): print("Available commands:", list(CommandRegistryMeta.commands.keys())) class ClearCommand(Command): @classmethod def execute(cls): print("Screen cleared!") # Simulate CLI command = "help" if command in CommandRegistryMeta.commands: CommandRegistryMeta.commands[command].execute()
This completes the full Metaclasses & Advanced OOP section — now you understand one of Python's most advanced concepts and when (and when not) to use it!
11. Design Patterns in Python
11.1 Singleton, Factory, Abstract Factory
Singleton Pattern Ensures a class has only one instance and provides a global point of access.
Classic way (using metaclass)
Python
class SingletonMeta(type): instances = {} def _call__(cls, args, *kwargs): if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class DatabaseConnection(metaclass=SingletonMeta): def init(self): self.connected = False def connect(self): if not self.connected: print("Connecting to database...") self.connected = True db1 = DatabaseConnection() db2 = DatabaseConnection() print(db1 is db2) # True – same instance
Pythonic alternative (module-level singleton) Most common in Python — just use a module!
Python
# database.py connection = None def getconnection(): global connection if connection is None: print("Creating new connection...") connection = "DB Connection Object" return connection
Factory Pattern Creates objects without specifying the exact class.
Python
class Button: def render(self): pass class WindowsButton(Button): def render(self): return "Render Windows-style button" class MacButton(Button): def render(self): return "Render Mac-style button" class GUIFactory: def create_button(self): pass class WindowsFactory(GUIFactory): def create_button(self): return WindowsButton() class MacFactory(GUIFactory): def create_button(self): return MacButton() # Usage def create_ui(factory: GUIFactory): button = factory.create_button() print(button.render()) factory = WindowsFactory() # or MacFactory() create_ui(factory)
Abstract Factory Creates families of related objects.
Pythonic: Use factories returning multiple related objects (often dicts or tuples).
Python
def get_theme_factory(theme): if theme == "dark": return { "button": "DarkButton", "checkbox": "DarkCheckbox", "background": "#1e1e1e" } else: return { "button": "LightButton", "checkbox": "LightCheckbox", "background": "#ffffff" } theme = get_theme_factory("dark") print(theme["background"]) # #1e1e1e
11.2 Observer, Strategy, Decorator Pattern
Observer Pattern (Publish-Subscribe) One object notifies many dependents when its state changes.
Pythonic way: Use callbacks / event system
Python
class Subject: def init(self): self._observers = [] def attach(self, observer): self._observers.append(observer) def detach(self, observer): self._observers.remove(observer) def notify(self, message): for observer in self._observers: observer.update(message) class NewsPublisher(Subject): def publish(self, headline): print(f"New headline: {headline}") self.notify(headline) class Subscriber: def init(self, name): self.name = name def update(self, message): print(f"{self.name} received: {message}") pub = NewsPublisher() sub1 = Subscriber("Anshuman") sub2 = Subscriber("Rahul") pub.attach(sub1) pub.attach(sub2) pub.publish("Python 3.14 released!") # Output: # New headline: Python 3.14 released! # Anshuman received: Python 3.14 released! # Rahul received: Python 3.14 released!
Modern Pythonic alternatives:
Use blinker library (signals)
Use asyncio events / queues
Use property + callbacks
Strategy Pattern Define a family of algorithms, encapsulate each, make them interchangeable.
Pythonic way: Pass functions / lambdas
Python
def discount_strategy(order_total): return order_total * 0.9 # 10% off def premium_strategy(order_total): return order_total - 500 if order_total > 5000 else order_total def checkout(total, discount_func): final = discount_func(total) print(f"Original: ₹{total} → Final: ₹{final}") checkout(6000, discount_strategy) # Final: ₹5400.0 checkout(6000, premium_strategy) # Final: ₹5500
Decorator Pattern Dynamically add responsibilities to objects.
Python already has decorators — use @ syntax!
Python
def add_tax(func): def wrapper(price): return func(price) * 1.18 # 18% GST return wrapper @add_tax def get_book_price(): return 500 print(get_book_price()) # 590.0
11.3 Pythonic Alternatives to Classic Patterns
Python's dynamic features make many GoF patterns unnecessary or over-engineered.
Classic PatternPythonic AlternativeWhy better in Python?SingletonModule-level variable / functionModules are naturally singletonsFactory / Abstract FactorySimple functions returning objects / dicts of factoriesFirst-class functions, duck typingStrategyPass functions / lambdas / callables as argumentsFunctions are first-class citizensDecoratorBuilt-in @decorator syntaxClean, readable, standardObserverCallbacks, blinker, asyncio events, property settersSimpler than full subject-observer hierarchyVisitorfunctools.singledispatch or match-casePattern matching + multimethodsCommandCallable objects / lambdasNo need for heavy class hierarchyTemplate Methodabc abstract methods + inheritanceOr use composition + hooks
Example: Pythonic Command Pattern
Python
def save_file(): print("File saved") def send_email(): print("Email sent") commands = { "save": save_file, "email": send_email } def execute(command_name): cmd = commands.get(command_name) if cmd: cmd() else: print("Unknown command") execute("save") # File saved execute("email") # Email sent
Final advice (2026):
Prefer simple functions, decorators, composition, and duck typing over heavy class hierarchies.
Use design patterns as inspiration — not rigid rules.
When in doubt → ask: "Can I solve this with a function or decorator instead of a big class structure?"
This completes the full Design Patterns in Python section — now you know how to apply classic patterns in a clean, Pythonic way!
12. Performance Optimization
12.1 Time & Space Complexity Basics
Time Complexity — How runtime grows with input size (n) Space Complexity — How memory usage grows with input size
Common Big-O notations (from best to worst):
NotationNameGrowth Rate Example (n = 10 → 1,000)When you see itO(1)ConstantSame time alwaysDictionary lookup, array accessO(log n)LogarithmicVery slow growthBinary search, balanced tree opsO(n)LinearDoubles when input doublesLooping once over listO(n log n)LinearithmicFast for large nEfficient sorting (TimSort)O(n²)Quadratic100× slower when n×10Nested loops (bubble sort, etc.)O(2ⁿ)ExponentialExplodes very fastRecursive Fibonacci (naive)
Quick examples:
Python
# O(1) – constant time def get_user(users, user_id): return users.get(user_id) # dict lookup # O(n) – linear time def find_max(lst): return max(lst) # loops once internally # O(n²) – quadratic (avoid for large n) def has_duplicates(lst): for i in range(len(lst)): for j in range(i+1, len(lst)): if lst[i] == lst[j]: return True return False # O(n log n) – good sorted_list = sorted(my_list) # uses TimSort
Rule of thumb (2026):
n ≤ 10³ → almost anything is fine
n ≈ 10⁵–10⁶ → avoid O(n²)
n ≥ 10⁷ → need O(n log n) or better
Use collections.deque, set, dict instead of lists for frequent lookups/removals
12.2 Profiling (cProfile, timeit)
timeit – Quick & accurate timing for small snippets
Python
import timeit # Compare list vs set lookup setup = "data = list(range(1000000))" stmt_list = "999999 in data" stmt_set = "999999 in set(data)" print(timeit.timeit(stmt_list, setup, number=100)) # slow print(timeit.timeit(stmt_set, setup, number=100)) # very fast
cProfile – Full program profiling (find bottlenecks)
Python
import cProfile def slow_function(): total = 0 for i in range(1000000): total += i ** 2 return total cProfile.run("slow_function()")
Output snippet (example):
text
ncalls tottime percall cumtime percall filename:lineno(function) 1000000 0.450 0.000 0.450 0.000 <string>:1(<genexpr>) 1 0.451 0.451 0.451 0.451 <string>:1(slow_function)
Better: Use snakeviz for visualization
Bash
pip install snakeviz python -m cProfile -o profile.out your_script.py snakeviz profile.out
line_profiler – Line-by-line timing (very useful)
Bash
pip install line_profiler
Python
@profile def slow_loop(): total = 0 for i in range(100000): total += i * i return total
Run with:
Bash
kernprof -l script.py python -m line_profiler script.py.lprof
12.3 Efficient Data Structures
Choosing the right structure can give 10×–1000× speedup.
Task / OperationRecommended StructureTime ComplexityWhy? / Alternative (avoid)Frequent lookups / membershipset or dictO(1) avgAvoid list (O(n))Ordered unique itemscollections.OrderedDict or dict (3.7+)O(1)—Fast append/pop from both endscollections.dequeO(1)Avoid list (O(n) for pop(0))Count occurrencescollections.CounterO(n)Avoid manual dict countingPriority queue / min-heapheapqO(log n) push/pop—Sorted list with fast insertionbisect + listO(log n) search, O(n) insertUse when n is smallLarge numerical data / matrix opsnumpy arrayVery fast (C)Avoid Python lists
Example speedup – membership check
Python
import time data_list = list(range(1_000_000)) data_set = set(data_list) start = time.time() 999999 in data_list # O(n) → slow print(time.time() - start) # ~0.1–0.5 sec start = time.time() 999999 in data_set # O(1) → instant print(time.time() - start) # ~0.0000001 sec
12.4 Caching & Memoization
Memoization — cache function results to avoid recomputation.
Built-in: @functools.lru_cache
Python
from functools import lru_cache @lru_cache(maxsize=128) # maxsize=None → unlimited def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(35)) # Instant (cached)
Manual cache (simple dict)
Python
def expensive_calc(n, cache={}): if n in cache: return cache[n] result = n 3 + n 2 + n # simulate heavy work cache[n] = result return result
Advanced: @functools.cache (Python 3.9+) Unlimited cache (no maxsize limit)
12.5 NumPy & Pandas for Speed
For numerical/data work — NumPy & Pandas are 10–100× faster than pure Python lists/dicts.
NumPy example – Vectorized operations
Python
import numpy as np # Slow Python loop lst = list(range(1_000_000)) result = [x**2 for x in lst] # ~100 ms # NumPy – blazing fast arr = np.arange(1_000_000) result = arr ** 2 # ~1–5 ms
Pandas for data frames
Python
import pandas as pd # Slow: loop over rows df = pd.DataFrame({"A": range(1000000)}) df["B"] = df["A"] 2 # slow if done with apply() # Fast: vectorized df["B"] = df["A"] 2 # very fast
When to switch to NumPy/Pandas:
Working with numbers, arrays, matrices → NumPy
Tabular data, filtering, grouping, CSV/Excel → Pandas
Avoid loops → use vectorized operations, broadcasting
Mini Project – Speed Comparison Tool
Python
import timeit import numpy as np def python_sum(n): return sum(range(n)) def numpy_sum(n): return np.arange(n).sum() n = 10_000_000 print("Python:", timeit.timeit(lambda: python_sum(n), number=1)) print("NumPy :", timeit.timeit(lambda: numpy_sum(n), number=1))
Output example:
text
Python: 0.45 seconds NumPy : 0.008 seconds
This completes the full Performance Optimization section — now you have the tools to measure bottlenecks, choose efficient structures, and write blazing-fast Python code!
13. Testing in Python
13.1 unittest vs pytest
Python has two dominant testing frameworks:
Featureunittest (built-in)pytest (third-party)Winner (2026)Included in PythonYesNo (pip install pytest)unittestSyntax styleClass-based, verboseSimple functions, minimal boilerplatepytestAssertionsself.assertEqual(), self.assertTrue(), etc.assert x == y (natural Python)pytestFixtures / setupsetUp(), tearDown(), setUpClass()@pytest.fixture (very powerful)pytestParameterized testsManual or unittest.subTest()@pytest.mark.parametrize (clean & readable)pytestPlugins & ecosystemLimitedHuge (pytest-django, pytest-asyncio, etc.)pytestMocking supportBuilt-in unittest.mockSame + pytest-mock pluginTieCommunity adoptionUsed in standard library & some enterprisesDominant in open-source & modern projectspytestLearning curveSlightly steeperVery easypytest
Recommendation (2026):
Use pytest for almost everything — it's faster to write, more readable, and has a massive ecosystem.
Use unittest only if:
You cannot install external packages (e.g., some corporate environments)
You are contributing to Python standard library or legacy code
13.2 Writing Unit Tests
With pytest (recommended)
Python
# tests/test_calculator.py def add(a, b): return a + b def test_add_positive(): assert add(2, 3) == 5 def test_add_negative(): assert add(-1, 1) == 0 def test_add_zero(): assert add(0, 0) == 0
Run tests:
Bash
pytest tests/ # discovers all test_*.py files pytest tests/test_calculator.py pytest -v # verbose output pytest --tb=short # shorter tracebacks
With unittest (built-in)
Python
# tests/test_calculator_unittest.py import unittest def add(a, b): return a + b class TestAddFunction(unittest.TestCase): def test_add_positive(self): self.assertEqual(add(2, 3), 5) def test_add_negative(self): self.assertEqual(add(-1, 1), 0) def test_add_zero(self): self.assertEqual(add(0, 0), 0) if name == '__main__': unittest.main()
Run:
Bash
python -m unittest tests/test_calculator_unittest.py python -m unittest discover # auto-discover
Best practices for unit tests:
Name tests clearly: test_function_scenario_expectation
Test one thing per test function
Use descriptive assertions
Keep tests fast & independent
Aim for high coverage (80%+ for important code)
13.3 Mocking (unittest.mock)
Mocking replaces real objects (external APIs, databases, files) with fake versions during tests.
Using unittest.mock (works with both pytest & unittest)
Python
# calculator.py import requests def get_weather(city): response = requests.get(f"https://api.weather.com/{city}") return response.json()["temp"] # tests/test_calculator.py (pytest) from unittest.mock import patch import pytest @patch("requests.get") def test_get_weather_success(mock_get): # Mock response mock_response = mock_get.return_value mock_response.json.return_value = {"temp": 28} mock_response.status_code = 200 result = get_weather("Muzaffarpur") assert result == 28 mock_get.assert_called_once_with("https://api.weather.com/Muzaffarpur")
Mocking file reading
Python
from unittest.mock import mock_open, patch @patch("builtins.open", new_callable=mock_open, read_data="42") def test_read_number(mock_file): with open("number.txt") as f: num = int(f.read()) assert num == 42
pytest fixtures + mocking (cleaner)
Python
import pytest from unittest.mock import patch @pytest.fixture def mock_requests(): with patch("requests.get") as mock: yield mock def test_get_weather(mock_requests): mock_requests.return_value.json.return_value = {"temp": 28} assert get_weather("Delhi") == 28
13.4 Test-Driven Development (TDD) Basics
TDD workflow:
Write a failing test (red)
Write minimal code to make test pass (green)
Refactor code (keep tests green)
Example: TDD for a simple calculator
Step 1: Write failing test
Python
# tests/test_math.py def test_subtract(): assert subtract(10, 4) == 6
Step 2: Make it pass (minimal code)
Python
def subtract(a, b): return a - b
Step 3: Add more tests & refactor
Python
def test_subtract_negative(): assert subtract(5, 10) == -5 def test_subtract_zero(): assert subtract(7, 0) == 7
Benefits of TDD:
Forces clear requirements
Produces testable, clean code
Reduces bugs
Gives confidence during refactoring
Pythonic TDD tools:
pytest + pytest-watch (pip install pytest-watch) → auto-run tests on file change
coverage.py → measure test coverage
tox → test across multiple Python versions
Mini Project – TDD for a User Validator
Python
# tests/test_validator.py def test_valid_email(): assert is_valid_email("anshuman@example.com") is True def test_invalid_email(): assert is_valid_email("anshuman@") is False def test_email_with_subdomain(): assert is_valid_email("ans@sub.example.co.in") is True
Start with failing tests, then implement is_valid_email() step-by-step using regex or string checks.
This completes the full Testing in Python section — now you can write reliable, well-tested code and adopt professional testing habits!
14. Popular Libraries & Real-World Tools
14.1 requests – HTTP & APIs
requests is the simplest and most popular library for making HTTP requests — used by almost every Python developer.
Install
Bash
pip install requests
Basic GET request
Python
import requests response = requests.get("https://api.github.com/users/AnshumanKumar07") print(response.status_code) # 200 print(response.json()["name"]) # Your GitHub name print(response.json()["public_repos"]) # Number of public repos
POST with JSON payload
Python
payload = {"title": "My Post", "body": "Hello from Python", "userId": 1} headers = {"Content-Type": "application/json"} response = requests.post( "https://jsonplaceholder.typicode.com/posts", json=payload, # automatically serializes to JSON headers=headers ) print(response.status_code) # 201 Created print(response.json())
Advanced: Sessions, timeouts, authentication
Python
session = requests.Session() session.headers.update({"Authorization": "Bearer your_token"}) try: response = session.get("https://api.example.com/data", timeout=5) response.raise_for_status() # raises exception on 4xx/5xx except requests.exceptions.RequestException as e: print(f"Request failed: {e}")
Best practices (2026):
Always use timeout=...
Use response.raise_for_status()
Prefer json= over data= for JSON
Use Session() for repeated calls (reuses connections)
14.2 BeautifulSoup & Scrapy – Web Scraping
BeautifulSoup – Best for simple/static scraping Scrapy – Best for large-scale, dynamic, structured scraping
BeautifulSoup (with requests)
Python
pip install beautifulsoup4 lxml
Python
from bs4 import BeautifulSoup import requests url = "https://example.com" response = requests.get(url) soup = BeautifulSoup(response.text, "lxml") # or "html.parser" # Find elements title = soup.find("h1").text print("Page title:", title) # Find all links for link in soup.find_all("a", href=True): print(link["href"]) # CSS selector articles = soup.select("article.news-item") for article in articles: print(article.find("h2").text)
Scrapy (full framework)
Bash
pip install scrapy scrapy startproject my_scraper
Basic spider example
Python
# my_scraper/spiders/news.py import scrapy class NewsSpider(scrapy.Spider): name = "news" start_urls = ["https://news.ycombinator.com/"] def parse(self, response): for post in response.css("tr.athing"): yield { "title": post.css("span.titleline a::text").get(), "link": post.css("span.titleline a::attr(href)").get(), "points": post.xpath("following-sibling::tr/td/span[@class='score']/text()").get() } # Follow next page next_page = response.css("a.morelink::attr(href)").get() if next_page: yield response.follow(next_page, self.parse)
Run:
Bash
scrapy crawl news -o news.json
When to choose:
Small/static site → BeautifulSoup + requests
Large/dynamic site, need login/pagination/exports → Scrapy
Legal note: Always respect robots.txt and website terms — scraping public data is usually fine, but avoid aggressive crawling.
14.3 pandas & NumPy – Data Manipulation
NumPy – Foundation for numerical computing (arrays, math) pandas – Excel-like data frames + powerful analysis
Install
Bash
pip install numpy pandas
NumPy basics
Python
import numpy as np arr = np.array([1, 2, 3, 4, 5]) print(arr * 2) # [ 2 4 6 8 10] print(arr.mean()) # 3.0 # 2D array (matrix) matrix = np.random.rand(3, 4) print(matrix.shape) # (3, 4) print(matrix.sum(axis=0)) # sum each column
pandas basics
Python
import pandas as pd # Read CSV/Excel df = pd.read_csv("sales.csv") # df = pd.read_excel("data.xlsx") print(df.head()) # first 5 rows print(df["revenue"].mean()) # average revenue # Filter & group high_sales = df[df["revenue"] > 10000] print(high_sales.groupby("region")["revenue"].sum()) # New column df["tax"] = df["revenue"] * 0.18 df.to_csv("processed_sales.csv", index=False)
Speed tip: Use vectorized operations — never loop over rows in pandas/NumPy.
14.4 Flask / FastAPI – Web Development
Flask – Lightweight, flexible micro-framework FastAPI – Modern, fast, async-first (most popular in 2026)
Flask example (simple API)
Python
from flask import Flask, jsonify, request app = Flask(__name__) @app.route("/api/greet", methods=["GET"]) def greet(): name = request.args.get("name", "World") return jsonify({"message": f"Hello, {name}!"}) if name == "__main__": app.run(debug=True)
FastAPI example (recommended 2026)
Python
# pip install fastapi uvicorn from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float @app.get("/greet") def greet(name: str = "World"): return {"message": f"Hello, {name}!"} @app.post("/items/") def create_item(item: Item): return {"item_name": item.name, "item_price": item.price}
Run:
Bash
uvicorn main:app --reload
Why FastAPI in 2026?
Automatic OpenAPI docs (Swagger UI)
Async support
Type hints → auto-validation
Very fast (Starlette + Pydantic)
14.5 SQLAlchemy / Django ORM – Databases
SQLAlchemy – Flexible, powerful ORM (used standalone or with FastAPI/Flask) Django ORM – Built into Django, very productive for full-stack apps
SQLAlchemy example (with SQLite)
Python
# pip install sqlalchemy from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() class User(Base): tablename = "users" id = Column(Integer, primary_key=True) name = Column(String) email = Column(String, unique=True) engine = create_engine("sqlite:///mydb.db") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() # Add user new_user = User(name="Anshuman", email="anshuman@example.com") session.add(new_user) session.commit() # Query users = session.query(User).all() for user in users: print(user.name, user.email)
Django ORM (inside Django project)
Python
# models.py from django.db import models class Book(models.Model): title = models.CharField(max_length=200) author = models.CharField(max_length=100) published_date = models.DateField() # Usage in shell/views Book.objects.create(title="Python Mastery", author="Anshuman", published_date="2026-01-01") books = Book.objects.filter(author="Anshuman")
Comparison (2026):
Need standalone ORM + async support → SQLAlchemy (with async engine)
Building full web app with admin, auth → Django ORM
FastAPI + SQLAlchemy = most modern stack
Mini Project – FastAPI + SQLAlchemy CRUD Create a simple REST API for a to-do list — combine requests, FastAPI, and SQLAlchemy.
This completes the full Popular Libraries & Real-World Tools section — now you have hands-on knowledge of the most important libraries used by Python developers in 2026!
15. Mini Advanced Projects & Best Practices
15.1 CLI Tool with argparse / click
argparse (built-in) vs click (modern & beautiful)
Recommended in 2026: Use click — cleaner syntax, better help messages, colors, progress bars.
Install
Bash
pip install click
Example: File stats CLI tool
Python
# file_stats.py import click from pathlib import Path import os @click.command() @click.argument("path", type=click.Path(exists=True), default=".") @click.option("--size", "-s", is_flag=True, help="Show file sizes") @click.option("--count", "-c", is_flag=True, help="Count files") def stats(path, size, count): """Show statistics about files in a directory.""" p = Path(path) files = list(p.rglob("*")) if count: click.echo(f"Total files: {len(files)}") if size: total_size = sum(f.stat().st_size for f in files if f.is_file()) click.echo(f"Total size: {total_size / 1024 / 1024:.2f} MB") if not (size or count): click.echo("Use --size or --count option (or both)") if name == "__main__": stats()
Run examples
Bash
python file_stats.py . --size --count python file_stats.py downloads/ -s -c python file_stats.py --help
Improvement ideas: Add subcommands (@click.group), progress bar (click.progressbar), output to JSON/CSV
15.2 Async Web Scraper
Goal: Scrape multiple pages concurrently with asyncio + aiohttp + BeautifulSoup
Install
Bash
pip install aiohttp beautifulsoup4
Code
Python
import asyncio import aiohttp from bs4 import BeautifulSoup from urllib.parse import urljoin async def fetch(session, url): async with session.get(url, timeout=10) as response: return await response.text() async def scrape_page(session, url): html = await fetch(session, url) soup = BeautifulSoup(html, "lxml") title = soup.title.string.strip() if soup.title else "No title" links = [urljoin(url, a["href"]) for a in soup.find_all("a", href=True)] return {"url": url, "title": title, "link_count": len(links)} async def main(start_url): async with aiohttp.ClientSession() as session: tasks = [scrape_page(session, start_url)] results = await asyncio.gather(*tasks, return_exceptions=True) for result in results: if isinstance(result, Exception): print(f"Error: {result}") else: print(f"URL: {result['url']}") print(f"Title: {result['title']}") print(f"Links found: {result['link_count']}\n") if name == "__main__": asyncio.run(main("https://example.com"))
Improvements:
Add recursive crawling with depth limit
Save results to JSON/CSV
Handle rate limiting & retries (aiohttp_retry)
Use asyncio.Semaphore to limit concurrent requests
15.3 Custom Decorator-based Logger
Goal: Create a decorator that logs function calls with arguments, return value, and execution time.
Python
import time import logging from functools import wraps logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) def log_execution(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() func_name = func.__name__ arg_str = ", ".join([f"{a!r}" for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]) logging.info(f"Calling {func_name}({arg_str})") try: result = func(*args, **kwargs) elapsed = time.perf_counter() - start logging.info(f"{func_name} returned {result!r} in {elapsed:.4f}s") return result except Exception as e: elapsed = time.perf_counter() - start logging.error(f"{func_name} raised {type(e).__name__}: {e} in {elapsed:.4f}s") raise return wrapper # Usage @log_execution def divide(a, b): return a / b divide(10, 2) # success # divide(10, 0) # logs error
Output example:
text
2026-03-05 17:12:45 [INFO] Calling divide(10, 2) 2026-03-05 17:12:45 [INFO] divide returned 5.0 in 0.0001s
Improvements: Add file logging, log level control, custom format, async support
15.4 Thread-Safe Counter Class
Goal: Create a thread-safe counter using locks.
Python
import threading class ThreadSafeCounter: def init(self, initial=0): self._value = initial self._lock = threading.Lock() def increment(self, step=1): with self._lock: self._value += step return self._value def decrement(self, step=1): with self._lock: self._value -= step return self._value @property def value(self): with self._lock: return self._value # Usage with threads counter = ThreadSafeCounter() def worker(): for in range(100000): counter.increment() threads = [threading.Thread(target=worker) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() print("Final count:", counter.value) # Exactly 1,000,000
Alternative (Python 3.9+): Use threading.atomic or queue.Queue for simpler cases.
15.5 Data Pipeline with Generators
Goal: Build memory-efficient ETL pipeline using generators.
Python
def read_lines(filename): """Generator: read file line by line""" with open(filename, "r", encoding="utf-8") as f: for line in f: yield line.strip() def filter_errors(lines): """Filter only ERROR lines""" for line in lines: if "ERROR" in line.upper(): yield line def parse_log(line): """Parse log line (simplified)""" parts = line.split(" - ") if len(parts) >= 2: return {"timestamp": parts[0], "message": parts[1]} return {"raw": line} def process_pipeline(filename): raw = read_lines(filename) errors = filter_errors(raw) parsed = (parse_log(line) for line in errors) for item in parsed: yield item # Usage for entry in process_pipeline("server.log"): print(entry)
Advantages: Processes one line at a time → works with huge files Improvements: Add yield from, error handling, save to database/JSON
15.6 PEP 8, PEP 257, Documentation & Git Workflow
PEP 8 – Style Guide (must follow)
4 spaces indentation
Line length: 88–100 chars (Black default)
Snake_case for variables/functions
CamelCase for classes
Spaces around operators: a = b + c
Import order: standard → third-party → local
Tools to enforce:
Bash
pip install black isort flake8 mypy black . # format isort . # sort imports flake8 . # lint mypy . # type check
PEP 257 – Docstrings
Python
def calculate_area(radius: float) -> float: """Calculate area of a circle. Args: radius: Radius of the circle (must be positive). Returns: Area in square units. Raises: ValueError: If radius is negative. """ if radius < 0: raise ValueError("Radius cannot be negative") return 3.14159 radius * 2
Git Workflow (recommended for solo/team)
git clone repo
git checkout -b feature/add-login
Work → commit often (git commit -m "Add login endpoint")
git push origin feature/add-login
Create Pull Request on GitHub/GitLab
Review → merge → delete branch
git pull origin main → git fetch --prune
Commit message style (Conventional Commits)
text
feat: add user registration endpoint fix: resolve division by zero error docs: update README with installation steps refactor: simplify authentication logic chore: update dependencies
This completes the full Mini Advanced Projects & Best Practices section — these projects will help you apply everything you've learned and build a strong portfolio!
16. Next Level Roadmap (2026+)
You’ve completed Python from zero to advanced — congratulations! 🎉 Now it’s time to specialize and build real-world skills that get you jobs, freelance work, or open-source contributions. Below is a practical, high-demand roadmap for 2026–2027.
16.1 Web Development (FastAPI, Django)
FastAPI is currently (2026) the #1 choice for modern Python web APIs — fast, async, automatic OpenAPI docs, type-safe with Pydantic.
Recommended Learning Path:
Build REST + async APIs with FastAPI
Use SQLAlchemy (async) or Tortoise-ORM for databases
Add authentication (JWT, OAuth2)
Deploy with Docker + Uvicorn/Gunicorn
Add tests (pytest + httpx)
Key Projects:
To-do list API with user auth
Blog API with CRUD + pagination
Real-time chat (WebSockets + FastAPI)
Resources:
Official FastAPI docs (excellent)
“FastAPI – A python framework for building APIs” (free course on YouTube by Sanjeev Thiyagarajan)
“Test-Driven Development with FastAPI and Docker” (free book on TestDriven.io)
Django – Still dominant for full-stack apps with admin panel, ORM, auth built-in.
When to choose:
FastAPI → APIs, microservices, modern startups
Django → Full websites with admin, rapid prototyping, enterprise
Projects:
Django: Personal blog with comments & admin
Django REST Framework (DRF) + React/Vue frontend
16.2 Data Science / Machine Learning
2026–2027 hot stack: Python + Polars (faster than pandas) + scikit-learn + PyTorch / TensorFlow + Hugging Face
Learning Path:
Master NumPy, pandas/Polars, Matplotlib/Seaborn/Plotly
Statistics & probability basics
scikit-learn (classification, regression, clustering)
Deep learning: PyTorch (preferred in 2026) or TensorFlow
Hugging Face Transformers → NLP, computer vision
MLOps basics (MLflow, DVC, BentoML)
Key Projects:
House price prediction (regression)
Customer churn classification
Image classification (transfer learning)
Sentiment analysis with BERT
Recommendation system (collaborative filtering)
Resources:
“Python for Data Analysis” (Wes McKinney – pandas creator)
“Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow” (Aurélien Géron)
fast.ai courses (free, practical deep learning)
Kaggle competitions (best practice platform)
16.3 DevOps & Automation
Goal: Automate deployment, infrastructure, CI/CD, monitoring
Key Tools (2026 standard):
Docker & Docker Compose
GitHub Actions / GitLab CI (free & powerful)
Kubernetes basics (minikube or kind for learning)
Terraform / Pulumi (IaC)
Ansible / Fabric for configuration
Prometheus + Grafana (monitoring)
Sentry / Rollbar (error tracking)
Learning Path:
Dockerize a FastAPI app
Set up GitHub Actions CI/CD pipeline
Deploy to Render / Railway / Fly.io (easiest)
Learn basic AWS/GCP/Azure (one of them)
Automate daily tasks with Python (scheduling with cron/APScheduler)
Projects:
Auto-deploy FastAPI app on push to GitHub
Dockerized Django + PostgreSQL + Nginx
Automated backup script for database/files
16.4 Contributing to Open Source
Contributing builds your portfolio, network, and skills faster than any course.
Step-by-step Guide (2026):
Create GitHub profile → pin your best projects
Find beginner-friendly repos:
“good first issue” or “help wanted” label
Popular: FastAPI, Django, scikit-learn, pandas, requests, black, Ruff
Start small: fix typos/docs, add tests, update dependencies
Read CONTRIBUTING.md carefully
Open issue first if adding feature
Submit clean PR with good commit messages
Respond to feedback politely
Best Repos for Beginners (2026):
fastapi/fastapi
encode/django-rest-framework
tiangolo/sqlmodel
psf/black
astral-sh/ruff
pandas-dev/pandas (good first issues)
Tip: Use GitHub’s “Explore” → “Topics” → “good-first-issue”
16.5 Recommended Books & Courses (2026 Updated)
Must-Read Books:
“Fluent Python” (2nd Edition) – Luciano Ramalho → deep Python mastery
“Python Cookbook” (3rd Edition) – David Beazley & Brian K. Jones → advanced patterns
“Effective Python” (2nd Edition) – Brett Slatkin → 90 best practices
“Clean Code” – Robert C. Martin (adapted to Python)
“Test-Driven Development with Python” – Harry Percival (free online)
Best Free/Paid Courses (2026):
FastAPI official tutorial (free)
“FastAPI – The Complete Course” by José Salvá (Udemy)
“Python – The Big Picture” by Corey Schafer (YouTube – free)
“Complete Python Developer” by Andrei Neagoie (Zero To Mastery)
“Deep Learning Specialization” by Andrew Ng (Coursera)
“Practical Deep Learning for Coders” by fast.ai (free & best)
“Python Testing with pytest” by Brian Okken (book + course)
Final Advice (from Anshuman’s Tutorial):
Build real projects → put them on GitHub
Deploy at least 3 apps (Render, Railway, Fly.io are free & easy)
Write READMEs, add tests, use CI/CD
Contribute to open source → even 1 merged PR is huge
Share your progress on LinkedIn / Twitter / Hashnode
Code daily — even 30 minutes
You now have a full roadmap from beginner → advanced → pro Python developer in 2026. Keep building, keep learning, and never stop asking questions! 🚀
Congratulations on completing the entire series!
Free Reading Alert! All my books are FREE on Kindle Unlimited or eBooks just ₹145!
Check now: https://www.amazon.in/stores/Anshuman-Mishra/author/B0DQVNPL7P
Start reading! 🚀
फ्री रीडिंग का मौका! मेरी सारी किताबें Kindle Unlimited में FREE या ईबुक सिर्फ ₹145 में!
अभी देखें: https://www.amazon.in/stores/Anshuman-Mishra/author/B0DQVNPL7P पढ़ना शुरू करें! 🚀🚀
These Python notes made complex concepts feel simple and clear.
Amy K.
★★★★★
ibm.anshuman@gmail.com
© 2026 CodeForge AI | Privacy Policy |Terms of Service | Contact | Disclaimer | 1000 university college list












