1.0 Object-Oriented Programming (OOP) Deep Dive
1.1 Classes and Objects
-
Class: Blueprint for creating objects. Defined with
classkeyword. -
Object: Instance of a class. Created by calling the class.
-
__init__(self, ...): Constructor method. Initializes instance attributes. Called automatically upon instantiation.class Dog: def __init__(self, name, breed): self.name = name # Instance variable self.breed = breed my_dog = Dog("Buddy", "Golden Retriever") # Instantiation
1.2 Instance vs. Class Variables
| Feature | Instance Variable | Class Variable |
|---|---|---|
| Scope | Unique to each object/instance | Shared across all instances |
| Location | Inside __init__ or other methods (self.var) |
Directly in class body |
| Modification | Changes affect only that instance | Changes affect all instances |
class Student:
school = "RGPV" # Class Variable
def __init__(self, name):
self.name = name # Instance Variable
1.3 Types of Methods
| Decorator | First Parameter | Access | Typical Use |
|---|---|---|---|
| None (Instance) | self |
Instance & Class vars | Operate on object state |
@classmethod |
cls |
Class vars only | Factory methods, alternative constructors |
@staticmethod |
None | Neither (unless passed) | Utility functions, namespacing |
class MyClass:
count = 0
def __init__(self):
type(self).count += 1
@classmethod
def get_count(cls):
return cls.count
@staticmethod
def is_valid(obj):
return hasattr(obj, 'id')
1.4 Inheritance
-
Single: One child inherits from one parent.
-
Multiple: One child inherits from multiple parents. Caution: Diamond problem.
-
Multilevel: Chain of inheritance (Grandparent -> Parent -> Child).
-
super(): Used to call methods from parent class. Essential in MRO.
1.5 Method Resolution Order (MRO) & super()
-
MRO: Order Python searches for methods in inheritance hierarchy. Uses C3 Linearization algorithm.
-
View MRO:
ClassName.__mro__orClassName.mro(). -
super(): Returns a proxy object to delegate method calls to parent class in the MRO. Crucial for cooperative multiple inheritance.class A: pass class B(A): pass class C(A): pass class D(B, C): pass print(D.__mro__) # (D, B, C, A, object)
1.6 Polymorphism
-
Duck Typing: "If it walks like a duck and quacks like a duck, it's a duck." Object suitability determined by presence of methods/attributes, not type.
def make_sound(animal): animal.speak() # Works for any object with .speak() -
Operator Overloading: Implement special dunder methods.
| Operator | Method | | :--- | :--- | |
+|__add__(self, other)| |-|__sub__(self, other)| |*|__mul__(self, other)| |==|__eq__(self, other)| |<|__lt__(self, other)| |len(obj)|__len__(self)| |str(obj)|__str__(self)| |repr(obj)|__repr__(self)|
1.7 Encapsulation
-
Public:
var(default). Accessible from anywhere. -
Protected:
_var(convention). Should be treated as non-public. Accessible but a hint. -
Private:
__var(name mangling). Python renames to_ClassName__var. Harder to access accidentally.class Secret: def __init__(self): self.public = 1 self._protected = 2 self.__private = 3
1.8 Properties (@property)
-
Decorator to define getters, setters, deleters for controlled access to attributes.
-
Allows validation or computation without changing the public interface.
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 must be positive") self._radius = value @property def area(self): # Read-only computed property return 3.14 * self._radius ** 2
1.9 Abstract Base Classes (ABCs)
-
From
abcmodule. Define abstract methods that must be overridden by subclasses. -
Cannot instantiate a class with unimplemented abstract methods.
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass class Rectangle(Shape): def __init__(self, w, h): self.w, self.h = w, h def area(self): return self.w * self.h # Implementation required # s = Shape() # TypeError: Can't instantiate abstract class
1.10 Data Classes (@dataclass)
-
Decorator (Python 3.7+) to automatically generate
__init__,__repr__,__eq__, etc. -
Reduces boilerplate for classes primarily storing data.
from dataclasses import dataclass @dataclass class Point: x: float y: float color: str = 'red' # Default value p = Point(1.0, 2.0) # Auto-generated __init__
[!TIP] Exam Focus: Be prepared to write code for
super()in multiple inheritance, implement a dunder method (like__add__), and convert a regular class to a@dataclass.
2.0 Advanced Functions & Functional Programming Tools
2.1 First-Class Functions & Closures
-
First-Class: Functions can be assigned to variables, passed as arguments, returned from other functions.
-
Closure: Inner function that captures and remembers variables from its enclosing scope, even after the outer function has finished execution.
def outer(msg): def inner(): # Closure print(msg) # Captures 'msg' from outer scope return inner closure_func = outer("Hello") closure_func() # Prints "Hello"
2.2 Lambda Functions
-
Anonymous, single-expression functions:
lambda arguments: expression. -
Used for short, throwaway functions, often with
map,filter,sorted.square = lambda x: x**2 pairs = [(1, 'one'), (2, 'two')] pairs.sort(key=lambda p: p[1]) # Sort by second element
2.3 Higher-Order Functions: map, filter, reduce
| Function | Purpose | Returns | Example |
|---|---|---|---|
map(func, iterable) |
Apply func to every item |
Iterator | list(map(str, [1,2,3])) -> ['1','2','3'] |
filter(func, iterable) |
Keep items where func(item) is True |
Iterator | list(filter(lambda x: x%2, [1,2,3])) -> [1,3] |
reduce(func, iterable) |
Cumulatively apply func (left to right) |
Single value | from functools import reduce; reduce(lambda a,b: a*b, [1,2,3,4]) -> 24 |
2.4 Comprehensions & Generator Expressions
-
List Comprehension:
[expr for item in iterable if condition] -
Dict Comprehension:
{key: value for item in iterable} -
Set Comprehension:
{expr for item in iterable} -
Generator Expression:
(expr for item in iterable)โ lazy evaluation, memory efficient.squares_gen = (x*x for x in range(10)) # Generator, not computed yet
2.5 Decorators
-
Function that takes another function as argument and returns a modified function.
-
Syntax:
@decoratorabove function definition.def my_decorator(func): def wrapper(): print("Before") func() print("After") return wrapper @my_decorator def say_hello(): print("Hello") -
Decorators with Arguments: Need an extra layer of nesting.
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): for _ in range(n): func(*args, **kwargs) return wrapper return decorator @repeat(3) def greet(): print("Hi")
2.6 functools Module
-
@wraps: Preserves metadata (__name__,__doc__) of original function in decorators.from functools import wraps def deco(func): @wraps(func) def wrapper(*args, **kwargs): ... -
lru_cache(maxsize): Memoization decorator for expensive pure functions (Least Recently Used cache).from functools import lru_cache @lru_cache(maxsize=128) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) -
partial(func, *args, **kwargs): Freeze some arguments of a function, creating a new function with fewer parameters.
2.7 itertools Module
-
Infinite Iterators:
count(start=0, step=1),cycle(iterable),repeat(object, times=None). -
Combinatoric Iterators:
-
product(*iterables, repeat=1): Cartesian product. -
permutations(iterable, r=None): r-length permutations, no repeated elements. -
combinations(iterable, r): r-length combinations, no repeated elements, order doesn't matter. -
combinations_with_replacement(iterable, r): Allows repeated elements.
-
[!TIP] Exam Focus: Differentiate between
map/filter(returns iterator) vs list comprehensions. Know when to use a generator expression for memory efficiency. Understandlru_cachefor recursive problems.
3.0 Modules, Packages, and Project Structure
3.1 The import System
-
Absolute Import: Full path from project root. Recommended.
from mypackage.mymodule import myfunction -
Relative Import: Using
.for current package,..for parent. Only within a package.from .sibling import func # Current package from ..parent import func # Parent package
3.2 __name__ == "__main__" Idiom
-
Code inside this block runs only when the script is executed directly (
python script.py), not when imported as a module. -
Standard pattern for making a module both importable and executable.
def main(): # program logic pass if __name__ == "__main__": main()
3.3 Packages
-
Package: Directory containing
__init__.py(can be empty) and submodules. -
Subpackage: Nested package directory.
-
__init__.py: Initializes package, can define__all__(list of public names forfrom package import *).
3.4 Namespace Packages (PEP 420)
-
Packages without
__init__.pyfile. -
Can be split across multiple locations on
sys.path. -
Created implicitly when Python finds a directory with modules but no
__init__.py.
3.5 Module Search Path (sys.path)
-
List of directory names Python searches for modules.
-
Order: Current directory โ
PYTHONPATHโ installation-dependent default. -
Can be modified at runtime (usually not recommended).
3.6 Virtual Environments (venv)
-
Isolated Python environment with its own
site-packages. -
Purpose: Manage project-specific dependencies without global conflicts.
-
Creation:
python -m venv myenv -
Activation:
-
Windows (cmd):
myenv\Scripts\activate.bat -
Unix/macOS:
source myenv/bin/activate
-
3.7 Dependency Management
-
pip: Package installer.pip install package,pip freeze > requirements.txt. -
requirements.txt: Lists exact package versions for reproducible environments.Flask==2.3.3 numpy>=1.24.0 -
Install all:
pip install -r requirements.txt.
3.8 Publishing Packages
-
setup.py/setup.cfg: Traditional setuptools configuration (metadata, dependencies). -
pyproject.toml: Modern standard (PEP 518, 621). Defines build system requirements and project metadata. Tools:setuptools,flit,poetry.
[!TIP] Common Pitfall: Confusing absolute and relative imports. Use absolute imports for clarity unless there's a strong reason for relative. Always use a virtual environment for projects.
4.0 File I/O and Context Management
4.1 File Modes
| Mode | Meaning | File Position | Creates New? |
|---|---|---|---|
'r' |
Read (default) | Start | No |
'w' |
Write (truncate) | Start | Yes |
'a' |
Append | End | Yes |
'x' |
Exclusive creation | Start | Yes, fails if exists |
'b' |
Binary mode | - | - |
't' |
Text mode (default) | - | - |
'+' |
Update (read & write) | Start | - |
- Common combinations:
'rt'(default),'wt','rb','w+b'.
4.2 Reading Files
-
f.read(size=-1): Readsizebytes/chars.-1/omitted reads entire file. -
f.readline(): Read single line (includes trailing\n). -
f.readlines(): Read all lines into list. -
Iteration (Best for large files):
for line in f:reads line by line lazily.with open('file.txt') as f: for line in f: # Memory efficient print(line.strip())
4.3 Writing Files
-
f.write(string): Write string to file. Returns number of characters written. -
f.writelines(iterable): Write each string from iterable. No newlines added automatically.
4.4 Working with Paths
-
os.path(legacy, string-based):import os.path as op op.join('dir', 'file.txt') op.exists('path') -
pathlib(modern, object-oriented โ Recommended):from pathlib import Path p = Path('dir') / 'file.txt' # Overloaded / p.exists() p.read_text() # Read entire file p.write_text('data') # Write string p.mkdir(parents=True, exist_ok=True)
4.5 The with Statement & Context Managers
-
Ensures resources (files, locks, connections) are properly acquired and released.
-
with open(...) as f:automatically callsf.close()on block exit, even on exceptions. -
Protocol: Object must have
__enter__()(returns resource) and__exit__()(handles cleanup) methods.
4.6 Creating Custom Context Managers
-
Class-based:
class Timer: def __enter__(self): import time; self.start = time.time() return self def __exit__(self, exc_type, exc_val, exc_tb): self.end = time.time() print(f"Elapsed: {self.end - self.start:.2f}s") with Timer(): # code to time pass -
contextlib.contextmanager(decorator for generator functions):from contextlib import contextmanager @contextmanager def managed_file(filename): f = open(filename, 'w') try: yield f # Resource provided to `with` block finally: f.close()
[!TIP] Exam Focus: Always use
withfor file operations. Knowpathlib.Pathmethods overos.path. Be able to write a simple custom context manager (both class and@contextmanagerstyles).
5.0 Advanced Exception Handling
5.1 Exception Hierarchy
BaseException
โโโ SystemExit
โโโ KeyboardInterrupt
โโโ GeneratorExit
โโโ Exception # Most built-in exceptions inherit from here
โโโ StopIteration
โโโ ArithmeticError (ZeroDivisionError, OverflowError)
โโโ LookupError (IndexError, KeyError)
โโโ OSError (FileNotFoundError, PermissionError)
โโโ RuntimeError
โโโ TypeError
โโโ ValueError
โโโ ... (many others)
5.2 try/except/else/finally
-
Order:
tryโ (one or moreexcept) โ (else?) โ (finally?). -
else: Runs iftryblock did not raise an exception. -
finally: Always runs (cleanup code), regardless of exception. Runs afterelseif present.try: result = 10 / 0 except ZeroDivisionError as e: print(f"Caught: {e}") else: print("No error") # Not executed here finally: print("Cleanup") # Always executed
5.3 Catching Multiple Exceptions
-
Multiple
exceptclauses:try: ... except TypeError: ... except ValueError: ... -
Exception tuple (handle same way):
try: ... except (TypeError, ValueError) as e: print(f"Type or Value error: {e}")
5.4 The as Keyword
-
Captures the exception instance for inspection.
try: int("abc") except ValueError as e: print(e.args) # ('invalid literal for int() with base 10: \'abc\'',) print(type(e)) # <class 'ValueError'>
5.5 Raising & Chaining Exceptions
-
raise: Re-raise current exception (inexceptblock) or raise new one. -
raise NewException from original_exception: Explicit exception chaining. Preserves original traceback.try: open("missing.txt") except FileNotFoundError as e: raise RuntimeError("Failed to open file") from e # Output shows both exceptions and "The above exception was the direct cause..."
5.6 Custom Exception Classes
-
Inherit from
Exception(or a more specific built-in).class InsufficientFundsError(Exception): def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__(f"Balance {balance}, tried to withdraw {amount}")
5.7 The warnings Module
-
Issue warnings (not errors) for potential issues.
import warnings warnings.warn("This is a warning", DeprecationWarning) -
Control filtering:
warnings.filterwarnings('ignore', category=DeprecationWarning).
[!TIP] Common Pitfall: Using a bare
except:(catchesBaseException, includingSystemExit/KeyboardInterrupt). Always specify exception type(s). Usefinallyfor critical cleanup (closing files, releasing locks).
6.0 Testing, Debugging, and Profiling
6.1 Unit Testing with unittest
-
Structure: Inherit from
unittest.TestCase. -
Methods start with
test_. -
Assertions:
assertEqual(a, b),assertTrue(x),assertRaises(exception). -
Setup/Teardown:
import unittest class TestMath(unittest.TestCase): def setUp(self): # Runs before *each* test self.value = 5 def test_something(self): self.assertEqual(self.value * 2, 10) def tearDown(self): # Runs after *each* test del self.value -
Run:
python -m unittest test_module.pyor discover.
6.2 pytest (Third-Party)
-
Simpler syntax. Functions, not necessarily classes.
-
Fixtures:
@pytest.fixturefor setup/teardown. Scope control (function,module,session). -
Assertions: Simple
assertstatements with rich introspection. -
Parametrization:
@pytest.mark.parametrize("input,expected", [(1,2), (3,4)]).
6.3 Test Coverage (coverage.py)
-
Measures how much code is executed by tests.
-
Run:
coverage run -m pytest, thencoverage report -m(shows missing lines). -
HTML report:
coverage html.
6.4 Debugging with pdb
-
Python Debugger. Insert
import pdb; pdb.set_trace()orbreakpoint()(Python 3.7+). -
Key Commands:
-
n(next),s(step into),c(continue) -
l(list code),p expr(print expression) -
q(quit),h(help)
-
-
Can also run script directly:
python -m pdb script.py.
6.5 Logging (logging module)
-
Levels (increasing severity):
DEBUG<INFO<WARNING<ERROR<CRITICAL. -
Basic Config:
logging.basicConfig(level=logging.INFO) -
Recommended Setup:
import logging logger = logging.getLogger(__name__) # Per-module logger logger.setLevel(logging.DEBUG) handler = logging.FileHandler('app.log') formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) logger.info("Application started")
6.6 Profiling
-
cProfile: Execution time, function calls.python -m cProfile -s cumulative my_script.py -
memory_profiler: Line-by-line memory usage. Decorate function with@profile. Run:python -m memory_profiler script.py.
[!TIP] Exam Focus: Know the basic
unitteststructure. Understand when to usepdbvs logging. Know the difference betweenDEBUGandINFOlevels.coverage.pymeasures line execution, not correctness.
7.0 Concurrency and Parallelism
7.1 Concurrency vs. Parallelism & GIL
-
Concurrency: Dealing with multiple tasks at once (may interleave on single CPU). I/O-bound tasks.
-
Parallelism: Doing multiple tasks simultaneously on multiple CPUs. CPU-bound tasks.
-
GIL (Global Interpreter Lock): Mutex in CPython that allows only one thread to execute Python bytecode at a time. Limits CPU-bound multi-threading but doesn't affect I/O or multi-processing.
7.2 Threading (threading)
-
Use Case: I/O-bound (network, file I/O). Overcomes GIL during I/O waits.
-
Basic Usage:
import threading def task(): print(threading.current_thread().name) t = threading.Thread(target=task, name="MyThread") t.start() t.join() # Wait for completion -
Synchronization Primitives (prevent race conditions):
-
Lock: Basic mutual exclusion.acquire(),release()(or usewith lock:). -
RLock: Re-entrant lock (same thread can acquire multiple times). -
Semaphore: Allowsnthreads concurrent access. -
Event,Condition,Barrier.
-
7.3 Multiprocessing (multiprocessing)
-
Use Case: CPU-bound tasks. Bypasses GIL by using separate memory space (processes).
-
Basic Usage:
from multiprocessing import Process def f(name): print(f'Hello {name}') p = Process(target=f, args=('Bob',)) p.start() p.join() -
Queue: Process-safe communication (unlike threading where data is shared). -
Pool: Manages worker processes.pool.map(func, iterable).
7.4 Asynchronous Programming (asyncio)
-
Use Case: High-concurrency I/O-bound (thousands of connections). Single-threaded event loop.
-
Coroutines:
async def func(): .... Defined withasync. -
await: Pauses coroutine, yields control to event loop until result ready. -
Event Loop: Runs tasks.
asyncio.run(main())(Python 3.7+).import asyncio async def fetch_data(): await asyncio.sleep(1) # Simulate I/O return {'data': 1} async def main(): task1 = asyncio.create_task(fetch_data()) task2 = asyncio.create_task(fetch_data()) res1, res2 = await asyncio.gather(task1, task2) asyncio.run(main())
7.5 Concurrent Futures (concurrent.futures)
-
High-level interface for async execution.
-
ThreadPoolExecutor: For I/O-bound.from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=4) as executor: future = executor.submit(func, arg) result = future.result() # Blocks until done -
ProcessPoolExecutor: For CPU-bound. -
executor.map(func, iterable)returns iterator of results.
[!TIP] Key Decision Flow:
I/O-bound? โ
threading(simple) orasyncio(high concurrency).
CPU-bound? โ
multiprocessingorProcessPoolExecutor.
Remember: GIL prevents CPU-bound threads from running in parallel.
8.0 Additional Advanced Topics
8.1 Metaclasses
-
Metaclass: Class of a class. Defines how classes behave. Default metaclass is
type. -
Custom Metaclass: Inherit from
type, override__new__(mcs, name, bases, attrs)or__init__. -
Use Case: API enforcement, automatic registration, ORM field mapping.
class Meta(type): def __new__(mcs, name, bases, attrs): attrs['registry'] = {} return super().__new__(mcs, name, bases, attrs) class MyClass(metaclass=Meta): pass
8.2 Descriptors
-
Object that defines any of
__get__,__set__,__delete__. -
Used to manage attribute access. Foundation for
@property,classmethod,staticmethod. -
Data Descriptor: Implements
__set__. Takes precedence over instance dict. -
Non-Data Descriptor: Only
__get__. Instance dict takes precedence.class Descriptor: def __get__(self, obj, objtype=None): return self.value def __set__(self, obj, value): self.value = value class MyClass: attr = Descriptor()
8.3 Type Hints (typing module)
-
Purpose: Static analysis (mypy), IDE autocomplete, documentation. Not enforced at runtime.
-
Basic:
def func(name: str, age: int) -> bool: ... -
Containers:
List[int],Dict[str, float](use fromtypingfor Python <3.9). -
Optional[T]:Union[T, None].def f(x: Optional[int] = None) -> None: ... -
Any: Disables type checking. -
Generics:
from typing import TypeVar, GenericT = TypeVar('T') class Box(Generic[T]): def __init__(self, item: T): self.item = item def get(self) -> T: return self.item -
TypedDict: For dictionaries with specific keys. -
Protocol: Structural subtyping (duck typing with types).
8.4 Working with Databases
-
DB-API 2.0 (
PEP 249): Standard for Python database drivers. -
sqlite3(built-in): Lightweight disk-based DB.import sqlite3 conn = sqlite3.connect('test.db') cur = conn.cursor() cur.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)") cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",)) conn.commit() for row in cur.execute("SELECT * FROM users"): print(row) conn.close() -
ORMs (e.g., SQLAlchemy): Map tables to classes. Higher-level, database-agnostic.
8.5 Introduction to Web Frameworks
-
Flask (Microframework): Lightweight, flexible. Routes via
@app.route('/').requestobject,render_template. -
Django (Full-stack): Batteries-included (ORM, admin, auth). MTV pattern (Model-Template-View).
urls.py,views.py,models.py. -
Core Concept: HTTP request/response cycle, routing, templating, (for Django) ORM and admin.
8.6 subprocess Module
-
Run external commands, capture output.
-
Preferred:
subprocess.run()(Python 3.5+).import subprocess result = subprocess.run(['ls', '-l'], capture_output=True, text=True) print(result.stdout) if result.returncode != 0: print(f"Error: {result.stderr}") -
Security: Avoid
shell=Truewith untrusted input (risk of shell injection).
[!TIP] Exam Focus: Metaclasses and descriptors are advanced; understand the concept and simple use cases. Type hints are increasingly important. For DBs, know the basic
sqlite3flow (connect, cursor, execute, commit, close).subprocess.runis the modern way to shell out.