UNIT 4: Advanced Python Programming & Applications
1. Object-Oriented Programming (OOP) Deep Dive
Core Concepts:
-
Class: Blueprint for objects. Defined with
classkeyword. -
Object: Instance of a class. Created via
ClassName(). -
__init__(): Constructor method. Initializes instance attributes. First parameter isself(reference to instance). -
self: Explicit reference to current instance (mandatory in method definitions).
Inheritance Types:
| Type | Description | Example |
|---|---|---|
| Single | One child inherits from one parent | class Child(Parent): |
| Multiple | Child inherits from multiple parents | class Child(Parent1, Parent2): |
| Multilevel | Chain of inheritance (grandparent → parent → child) | class GrandChild(Child): |
Polymorphism:
-
Method Overriding: Child redefines parent's method.
-
Duck Typing: "If it walks like a duck...". Object suitability determined by methods/attributes, not class type.
Encapsulation & Access Control:
-
Public: No underscore (default). Accessible anywhere.
-
Protected: Single underscore
_attr. Convention: internal use. -
Private: Double underscore
__attr. Triggers name mangling:__attr→_ClassName__attr.
Special (Dunder) Methods:
| Method | Purpose | Example |
|---|---|---|
__str__() |
Readable string representation (for users) | print(obj) |
__repr__() |
Unambiguous string (for developers) | repr(obj) |
__len__() |
Length via len(obj) |
return len(self.data) |
__getitem__() |
Indexing obj[key] |
return self.data[key] |
Properties & Decorators:
-
@property: Turns method into read-only attribute.class Circle: def __init__(self, r): self._radius = r @property def radius(self): return self._radius @radius.setter def radius(self, value): self._radius = value -
@classmethod: Receivescls(class). Used for factory methods. -
@staticmethod: Noself/cls. Utility function inside class.
Abstract Base Classes (ABCs):
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): pass # Must be overridden
[!TIP] Abstract methods cannot have implementation. Subclass must override all
@abstractmethods to instantiate.
2. File Handling & Data Persistence
File Modes:
| Mode | Description |
|---|---|
r |
Read (default) |
w |
Write (truncates) |
a |
Append |
b |
Binary mode |
+ |
Update (read + write) |
x |
Exclusive creation |
Context Manager (with):
with open('file.txt', 'r') as f:
data = f.read() # Auto-closes file after block
[!TIP] Always use
withfor file operations. Prevents resource leaks if exception occurs.
Text File Operations:
-
f.read(size): Read size bytes (or all if omitted). -
f.readline(): Read single line (includes\n). -
f.readlines(): Read all lines → list. -
f.write(str): Write string. Returns number of characters written. -
f.writelines(list): Write list of strings (no separators added).
CSV Handling (csv module):
import csv
# Reading
with open('data.csv') as f:
reader = csv.DictReader(f) # Returns dict per row
for row in reader: print(row['column'])
# Writing
with open('out.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['col1', 'col2'])
JSON Handling (json module):
import json
# Serialization (Python → JSON string/file)
json_str = json.dumps(obj, indent=4) # Pretty print
json.dump(obj, file) # Write to file
# Deserialization (JSON → Python)
obj = json.loads(json_str)
obj = json.load(file)
[!TIP] JSON supports: dict, list, str, int, float, bool, None. Custom objects need
defaulthook.
Pickle Module (Object Serialization):
import pickle
with open('data.pkl', 'wb') as f:
pickle.dump(obj, f) # Serialize
with open('data.pkl', 'rb') as f:
obj = pickle.load(f) # Deserialize
[!WARNING] Security Risk: Unpickling untrusted data can execute arbitrary code. Never unpickle from unknown sources.
File Paths:
-
os.path(legacy):os.path.join(),os.path.exists(). -
pathlib(modern, OOP):from pathlib import Path p = Path('folder') / 'file.txt' p.exists(), p.read_text(), p.write_text()
3. Exception Handling & Custom Errors
Block Structure:
try:
# Risky code
except SpecificError as e:
# Handle specific error
except (Error1, Error2) as e:
# Handle multiple errors
else:
# Runs if NO exception
finally:
# Always runs (cleanup)
Raising Exceptions:
raise ValueError("Invalid input")
# Exception chaining (preserves original context)
raise CustomError("msg") from original_exception
Built-in Hierarchy (Key Branches):
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception ← Most errors inherit from here
├── StopIteration
├── ArithmeticError (ZeroDivisionError, OverflowError)
├── LookupError (IndexError, KeyError)
├── OSError (FileNotFoundError, PermissionError)
└── TypeError, ValueError, RuntimeError, etc.
Custom Exceptions:
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Balance {balance}, tried to withdraw {amount}")
Best Practices:
-
Catch specific exceptions, not bare
except:. -
Use
finallyfor cleanup (close files, release locks). -
Log exceptions (
logging.exception()). -
Avoid empty
except:blocks.
4. Modules, Packages, and Distribution
Importing:
import module # Access via module.func()
from module import func # Direct access: func()
from module import * # Avoids namespace pollution
# Relative imports (inside package)
from .sibling import func
from ..parent import func
Package Structure:
mypackage/
├── __init__.py # Can be empty; marks directory as package
├── module1.py
├── subpackage/
│ ├── __init__.py
│ └── module2.py
[!TIP]
__init__.pycan initialize package-level variables or controlfrom package import *.
Namespace Packages (PEP 420):
-
No
__init__.pyrequired. -
Spread across multiple directories.
-
Created automatically when multiple paths contribute to same package name.
Key Standard Library Modules:
-
os: OS interactions (env, paths, processes). -
sys: System parameters (argv, path, exit). -
datetime: Date/time objects. -
collections: Specialized containers (deque,Counter,defaultdict). -
itertools: Iterator building blocks (chain,cycle,combinations).
Third-Party Packages:
pip install package_name
pip install -r requirements.txt # Install from list
requirements.txtformat:package==versionorpackage>=version.
Virtual Environments:
# venv (built-in)
python -m venv myenv
source myenv/bin/activate # Linux/Mac
myenv\Scripts\activate # Windows
# conda (cross-language)
conda create -n myenv python=3.9
conda activate myenv
[!TIP] Always use virtual environments to isolate project dependencies.
5. Advanced Language Features
Decorators:
def decorator(func):
from functools import wraps
@wraps(func) # Preserves metadata
def wrapper(*args, **kwargs):
# Pre-processing
result = func(*args, **kwargs)
# Post-processing
return result
return wrapper
@decorator
def my_func(): pass
[!TIP]
@functools.wrapsis essential to maintain original function's name/docstring.
Generators:
-
Function: Uses
yield. Returns generator object (lazy evaluation).def count_up_to(n): i = 0 while i < n: yield i i += 1 -
Expression:
(x**2 for x in range(10)). -
Memory efficient for large/infinite sequences.
Iterators:
class Counter:
def __init__(self, low, high):
self.current = low
self.high = high
def __iter__(self):
return self
def __next__(self):
if self.current > self.high:
raise StopIteration
else:
self.current += 1
return self.current - 1
Custom Context Managers:
-
Class-based:
class ManagedFile: def __init__(self, name): self.name = name def __enter__(self): self.file = open(self.name, 'w') return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close() -
@contextmanager(simpler):from contextlib import contextmanager @contextmanager def managed_file(name): f = open(name, 'w') try: yield f finally: f.close()
Regular Expressions (re module):
| Function | Purpose |
|---|---|
re.search(pattern, string) |
Find first match anywhere |
re.match(pattern, string) |
Match only at start |
re.findall(pattern, string) |
Return all matches as list |
re.sub(pattern, repl, string) |
Replace matches |
re.compile(pattern) |
Compile for reuse |
Flags: re.IGNORECASE, re.MULTILINE, re.DOTALL |
Common Patterns:
-
\d: digit,\w: word char,\s: whitespace. -
+: 1+,*: 0+,?: 0-1. -
^: start,$: end. -
(group): Capturing group.\1,\2backreferences. -
(?:...): Non-capturing group.
Type Hints (typing module):
from typing import List, Dict, Callable, TypeVar, Generic, Optional
def process(items: List[int]) -> Dict[str, int]:
...
T = TypeVar('T') # Generic type
class Box(Generic[T]):
def __init__(self, item: T): self.item = item
def get(self) -> T: return self.item
# Union types
def func(x: int | str) -> None: ...
# Optional (same as Union[T, None])
def func(x: Optional[int] = None) -> None: ...
[!TIP] Type hints are not enforced at runtime (unless using
mypy). They improve readability and IDE support.
6. Concurrency and Parallelism
Threading (threading):
-
GIL (Global Interpreter Lock): Only one thread executes Python bytecode at a time. Limits CPU-bound parallelism.
-
Use for I/O-bound tasks (network, file ops).
import threading
lock = threading.Lock()
def thread_func():
with lock: # Synchronization
# Critical section
t = threading.Thread(target=thread_func)
t.start()
t.join()
Multiprocessing (multiprocessing):
-
Bypasses GIL. Separate memory space.
-
Use for CPU-bound tasks.
from multiprocessing import Pool
def compute(x): return x*x
with Pool(4) as p:
results = p.map(compute, [1,2,3,4])
- IPC: Queues, Pipes, shared memory.
Asyncio (Single-threaded concurrency):
-
Event loop, coroutines (
async def),await. -
Cooperative multitasking. I/O-bound.
import asyncio
async def fetch_data():
await asyncio.sleep(1) # Simulate I/O
return "data"
async def main():
task1 = asyncio.create_task(fetch_data())
task2 = asyncio.create_task(fetch_data())
await task1; await task2
asyncio.run(main())
concurrent.futures (High-level API):
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# Thread pool (I/O-bound)
with ThreadPoolExecutor(max_workers=4) as executor:
future = executor.submit(func, arg)
result = future.result()
# Process pool (CPU-bound)
with ProcessPoolExecutor() as executor:
results = list(executor.map(func, iterable))
[!TIP] Choose threading for I/O, multiprocessing for CPU, asyncio for many network connections.
7. Testing and Debugging
Unit Testing (unittest):
import unittest
class TestMath(unittest.TestCase):
def setUp(self): # Runs before each test
self.value = 5
def test_add(self):
self.assertEqual(1+1, 2)
def test_raises(self):
with self.assertRaises(ValueError):
raise ValueError()
if __name__ == '__main__':
unittest.main()
Pytest (Simpler):
-
Test files:
test_*.pyor*_test.py. -
Functions:
def test_func(): assert .... -
Fixtures: Setup/teardown via
@pytest.fixture. -
Parametrization:
@pytest.mark.parametrize("x,y", [(1,2),(3,4)]).
Debugging:
-
pdb:import pdb; pdb.set_trace() # Breakpoint # Commands: n(ext), s(tep), c(ontinue), l(ist), p(rint) -
Logging:
import logging logging.basicConfig(level=logging.DEBUG) logging.debug("Value: %s", x) # Lazy formatting
Code Coverage (coverage.py):
coverage run -m pytest
coverage report -m # Show missing lines
coverage html # Generate HTML report
[!TIP] Aim for high coverage but focus on meaningful tests (edge cases, error paths).
8. Performance Optimization & Profiling
Profiling:
-
cProfile: Deterministic profiling.python -m cProfile -s cumtime script.py -
timeit: Small code snippets.import timeit timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
Memory Management:
-
sys.getsizeof(obj): Memory in bytes (shallow). -
gcmodule:gc.collect(),gc.get_objects(). -
Generators reduce memory vs lists.
Efficient Data Structures (collections):
| Class | Use Case |
|---|---|
namedtuple |
Immutable tuple with named fields |
deque |
Fast appends/pops from both ends |
Counter |
Count hashable objects (multiset) |
defaultdict |
Dict with default factory (no KeyError) |
Lazy Evaluation:
-
Generators (
yield) produce values on-demand. -
Iterator expressions
(x for x in ...)vs list comprehensions[x for x in ...]. -
Saves memory for large datasets.
9. Popular Libraries for Engineering/Scientific Computing
NumPy:
-
ndarray: Homogeneous, fixed-size, multi-dimensional. -
Vectorization: Operations on entire arrays (no Python loops).
-
Broadcasting: Implicit element-wise operations on arrays of different shapes.
import numpy as np a = np.array([[1,2],[3,4]]) b = np.array([10,20]) result = a + b # b broadcasted to [[10,20],[10,20]] -
Basic linear algebra:
np.dot(),np.linalg.inv().
Pandas:
-
Series: 1D labeled array. -
DataFrame: 2D table (columns of Series).
import pandas as pd
df = pd.read_csv('data.csv') # Load
df[df['col'] > 5] # Filter
df.groupby('category').mean() # Group
Matplotlib:
import matplotlib.pyplot as plt
x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x), label='sin') # Line plot
plt.scatter(x, np.cos(x)) # Scatter
plt.hist(data, bins=30) # Histogram
plt.subplot(2,1,1) # Subplots
plt.xlabel('X'); plt.legend()
plt.show()
SciPy (Intro):
-
Builds on NumPy.
-
Modules:
scipy.optimize(minimization),scipy.integrate(numerical integration),scipy.interpolate.
10. Web Development Fundamentals (Optional/Introductory)
Flask (Microframework):
from flask import Flask, jsonify, render_template
app = Flask(__name__)
@app.route('/') # Routing
def home():
return render_template('index.html') # Template
@app.route('/api/data')
def api():
return jsonify({'key': 'value'}) # JSON response
if __name__ == '__main__':
app.run(debug=True)
- MVC Pattern: Models (data), Views (templates), Controllers (routes).
REST APIs:
-
HTTP Methods:
GET(read),POST(create),PUT(update),DELETE. -
Stateless. JSON payloads.
-
Flask-RESTful/FastAPI simplify creation.
Database (SQLite with sqlite3):
import sqlite3
conn = sqlite3.connect('app.db')
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",)) # Parameterized!
conn.commit()
[!TIP] Always use parameterized queries to prevent SQL injection.
ORM (SQLAlchemy):
-
Maps Python classes to database tables.
-
Abstraction layer:
session.add(obj),session.query(Model).filter_by(...).
11. Code Quality and Best Practices
PEP 8 Key Rules:
-
Indentation: 4 spaces (no tabs).
-
Line Length: ≤ 79 chars (code), ≤ 72 (comments).
-
Naming:
-
snake_casefor functions/variables. -
CamelCasefor classes. -
UPPER_SNAKE_CASEfor constants.
-
-
Imports: Standard → third-party → local. One per line.
-
Whitespace: Around operators, after commas, but not inside brackets.
Docstrings:
-
Google Style:
def func(arg1, arg2): """Short description. Args: arg1 (int): Description. arg2 (str): Description. Returns: bool: Description. """ -
Access via
help(func)or IDE tooltips.
Static Analysis:
-
pylint: Checks for errors, style, refactoring. -
flake8: Combines PyFlakes (errors), pycodestyle (PEP 8), McCabe (complexity). -
mypy: Type checking using type hints.mypy script.py # Reports type mismatches
Packaging:
-
setup.py(legacy) orpyproject.toml(modern). -
Build:
python setup.py sdist bdist_wheel. -
Upload to PyPI:
twine upload dist/*.
12. Security Considerations
Common Vulnerabilities (Web Context):
-
Injection: SQL, command, LDAP. Mitigation: Parameterized queries, input validation.
-
XSS (Cross-Site Scripting): Inject scripts into web pages. Mitigation: Escape user input in templates.
-
CSRF (Cross-Site Request Forgery): Unauthorized actions. Mitigation: CSRF tokens.
Secure Coding Practices:
-
Input Validation: Validate type, length, format, range. Use
pydanticfor data validation. -
Avoid
eval(): Arbitrary code execution. Useast.literal_eval()for safe literals. -
Secrets Management: Never hardcode passwords/keys. Use environment variables or secret managers.
-
Least Privilege: Run processes with minimal permissions.
Dependency Security:
-
Regularly update packages.
-
Scan for vulnerabilities:
pip-audit # Audits installed packages safety check # Checks against known vulnerabilities -
Use
requirements.txtwith pinned versions (package==1.2.3) for reproducibility.
[!TIP] Security is layered. Validate inputs, use parameterized queries, keep dependencies updated, and never trust user data.