🐍
CORE PYTHON
ULTIMATE MASTER NOTES
Complete Theory • Definitions • Examples • Execution Traces • Practice
Beginner → Advanced | 2025 Edition
Sources: GFG • W3Schools • PDF Notes • Anthropic AI
PART 1
INTRODUCTION TO PYTHON & FOUNDATIONS
Chapter 1: What is Python?
ℹ Definition (GFG / W3Schools)
Python is a high-level, general-purpose, interpreted, dynamically-typed programming language.
Created by Guido van Rossum and first released in 1991.
Python emphasises code readability — its syntax allows programmers to express concepts
in fewer lines of code compared to C++ or Java.
It supports multiple programming paradigms: procedural, object-oriented, and functional.
1.1 Key Characteristics of Python
Feature Explanation
High-Level Language Closer to human language. Abstracts hardware details
from the programmer.
Interpreted Code is executed line by line by the Python interpreter.
No separate compile step.
Dynamically Typed Variable types are determined at runtime. No need to
declare types explicitly.
General Purpose Used in web development, data science, AI, automation,
scripting, and more.
Object-Oriented Supports classes, objects, inheritance, polymorphism,
encapsulation.
Open Source Free to use, distribute, and modify. Large community
support.
Portable Write once, run anywhere — same code runs on
Windows, Linux, and macOS.
Extensible Python can be extended with C/C++ modules for
performance-critical tasks.
Large Standard Library Comes with batteries included — modules for almost
everything.
Easy to Learn Clean syntax, minimal boilerplate, beginner-friendly.
1.2 History & Timeline of Python
Year / Version Milestone
1989 Guido van Rossum began writing Python as a hobby project over Christmas.
1991 — v0.9 First public release. Included functions, exceptions, core data types.
1994 — v1.0 Added lambda, map, filter, reduce functional tools.
2000 — v2.0 List comprehensions, garbage collector, Unicode support added.
2008 — v3.0 Major redesign — print() function, better Unicode, bytes/str split.
2010 — v2.7 Last Python 2 release. Supported until January 1, 2020.
2016 — v3.6 f-strings (PEP 498), variable annotations, ordered dicts.
2019 — v3.8 Walrus operator :=, positional-only parameters /
2021 — v3.10 match-case structural pattern matching (like switch in other langs).
2023 — v3.12 Faster interpreter, improved error messages, enhanced f-strings.
1.3 The Zen of Python — PEP 20
What is PEP 20? PEP stands for Python Enhancement Proposal. PEP 20 contains 19 guiding aphorisms written by
Tim Peters that describe the core philosophy behind Python's design. Run import this in the REPL to see
them.
▸ import this — Zen of Python
>>> import this
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
1.4 Python Use Cases
Domain How Python is Used Popular Tools/Libraries
Data Science Analysing, visualising, and interpreting Pandas, Matplotlib, Seaborn
large datasets
Machine Learning / AI Building and training ML models TensorFlow, PyTorch, scikit-learn
Web Development Building backend servers and APIs Django, Flask, FastAPI
Automation / Scripting Automating repetitive tasks, file operations os, shutil, subprocess
Cybersecurity Penetration testing, network scanning Scapy, Requests
Game Development Building 2D games Pygame
Desktop Apps GUI applications Tkinter, PyQt
DevOps / Cloud Infrastructure automation, CI/CD Ansible, Boto3 (AWS)
1.5 Python Execution Pipeline
How does Python run your code? When you run a .py file, Python does NOT execute it directly on the CPU. It
goes through a multi-stage transformation pipeline:
▸ Pipeline
PYTHON EXECUTION PIPELINE
┌─────────────────────────────────────────────────────┐
│ Your Source Code (.py file) │
│ e.g. print('Hello') │
└─────────────────────┬───────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ [1] TOKENIZER / LEXER │
│ Splits source code into tokens: │
│ NAME:'print' OP:'(' STRING:'Hello' OP:')' │
└─────────────────────┬───────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ [2] PARSER │
│ Checks grammar rules. Builds an Abstract Syntax │
│ Tree (AST) — a tree representation of the code │
└─────────────────────┬───────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ [3] COMPILER │
│ Converts AST into BYTECODE (.pyc files stored in │
│ __pycache__/ folder) — platform independent │
└─────────────────────┬───────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ [4] PVM — Python Virtual Machine │
│ Reads bytecode instruction by instruction │
│ and executes each one │
└─────────────────────┬───────────────────────────────┘
↓
OUTPUT
1.6 Python Versions: 2 vs 3
Feature Python 2 Python 3
print statement print 'Hello' (statement) print('Hello') (function)
Integer division 5/2 → 2 (floor) 5/2 → 2.5 (true division)
Unicode str is bytes; unicode is separate str is Unicode by default
input() raw_input() returns str; input() input() always returns str
evaluates
range() range() returns list range() returns lazy iterator
Support End of life: January 1, 2020 Active development
Syntax print x print(x)
1.7 CPython vs Other Implementations
Implementation Description
CPython Reference implementation. Written in C. Most widely used. This guide refers to
CPython.
PyPy JIT-compiled Python. Up to 10x faster for CPU-bound tasks. Drop-in replacement.
Jython Python implemented on the Java Virtual Machine. Can use Java libraries.
IronPython Python for the .NET/CLR platform. Can use .NET libraries.
MicroPython Stripped-down Python for microcontrollers and embedded systems (Raspberry Pi
Pico).
Chapter 2: Installation
2.1 Installing Python on Windows
⚙ Step-by-Step Guide
Step 1 — DOWNLOAD
Visit [Link] and click 'Download Python'.
Choose the latest stable version (e.g., Python 3.12.x).
Step 2 — RUN INSTALLER
Double-click the downloaded .exe file.
⚠️ IMPORTANT: Check the box 'Add Python to PATH' BEFORE clicking Install.
This allows you to run 'python' from any terminal.
Click 'Install Now'.
Step 3 — COMPLETE
Wait for setup to finish. Click 'Close'.
Step 4 — VERIFY
Open Command Prompt (Win + R → type 'cmd' → Enter).
Type: python --version
Expected output: Python 3.12.x
▸ Verify Installation
# Verify Python installation in terminal
python --version
# Output: Python 3.12.3
# Also verify pip (package installer)
pip --version
# Output: pip 24.x.x from ...
2.2 Installing Anaconda
What is Anaconda? Anaconda is a free, open-source distribution of Python and R for scientific computing. It
comes pre-packaged with Python, Jupyter Notebook, Spyder IDE, conda package manager, and 250+ data
science libraries.
📦 Anaconda Installation Steps
Step 1 — Download from [Link] → Anaconda Individual Edition
Step 2 — Run the .exe installer → Click Next → I Agree
Step 3 — Choose 'Just Me' (recommended) → Select install location
Step 4 — Optionally check 'Add Anaconda to PATH' → Click Install
Step 5 — Click Next → Finish
Step 6 — Verify: Open Anaconda Prompt, type: conda --version
Tool Description
Jupyter Notebook Browser-based interactive coding environment. Used heavily in data science.
Spyder Scientific Python IDE built into Anaconda. Good for data analysis.
conda Package and environment manager. Handles dependencies without conflicts.
IPython Enhanced interactive Python shell (more features than standard REPL).
2.3 Running Python — 4 Ways
Method How When to Use
REPL / Interactive Type 'python' in terminal → >>> Quick experiments, testing snippets
prompt appears
Script File Create [Link] → run: python [Link] Larger programs, repeatable code
Jupyter Notebook Start via Anaconda → New → Python Data science, visualisation, education
3
IDE (VS Code/PyCharm) Open file → click Run button Professional development
Chapter 3: Basic Programming Terms
3.1 What is Programming?
Definition: Programming is the process of writing a set of instructions that tell a computer what to do and how
to do it in order to solve a specific problem. Since computers cannot understand human language, programming
languages serve as an intermediary — they are precise enough for machines to execute yet readable enough for
humans to write and maintain.
💡 Key Insight
A computer is a very obedient but completely literal machine.
It does EXACTLY what you tell it — no more, no less.
If your instructions have a logical error, the computer will faithfully execute that error.
Programming = the art of giving perfectly precise instructions.
3.2 What is a Computer Program?
Definition: A computer program is a set of instructions (the smallest unit of execution) written in a
programming language that tells a computer how to perform a specific task to produce a specific result. Each
individual instruction in a program is called a statement.
3.3 Programming Language
Definition: A programming language is a formal set of rules, symbols, and syntax used to write instructions that
a computer can understand and execute. It bridges human thinking and machine execution.
Types of Programming Languages
Category Sub-type Examples Characteristics
Low-Level Machine Language Binary (0s & 1s) Directly understood by CPU.
Hardware-dependent. Very fast. No
translator needed.
Low-Level Assembly Language x86 ASM, MIPS Uses symbolic mnemonics (MOV,
ADD). Translated by an Assembler.
Processor-specific.
High-Level Compiled Languages C, C++, Go, Rust Translated by compiler before
execution. Faster at runtime.
High-Level Interpreted Python, Ruby, JS Executed line-by-line by interpreter.
Languages More flexible. Easier debugging.
High-Level Hybrid Java, C# Compiled to bytecode, then
interpreted by VM (JVM/.NET CLR).
Property Low-Level Language High-Level Language
Closeness to hardware Very close Far (abstracted)
Readability Hard to understand Easy to read and write
Execution speed Very fast Slower (interpreter overhead)
Portability Not portable (hardware-specific) Portable (runs on any OS)
Memory management Manual Automatic (garbage collection)
Examples Assembly, Machine Code Python, Java, C++, JavaScript
3.4 Source Code, Object Code & Executable Code
Type Written By Readable? Executable? File Extension
Source Code Programmer Yes (human- No (must be [Link], file.c, [Link]
readable) translated)
Object Code Compiler/Assembler No (machine No (must be [Link] (Windows)
bytes) linked)
Executable Code Linker No Yes (directly [Link] (Windows)
runnable)
Source Code: The human-readable program written by a programmer using a programming language. Contains
comments, variable names, and logical flow. Easy to modify.
Object Code: The intermediate machine-level code produced by a compiler from source code. It is machine-
understandable but not yet directly executable because it still needs to be linked with libraries.
Executable Code: The final, fully-linked machine code that the operating system can load into memory and
execute directly. On Windows it has a .exe extension.
▸ Code Transformation Pipeline
Source Code (.py / .c / .java)
↓ Compiler / Interpreter
Object Code (.obj) — intermediate
↓ Linker
Executable Code (.exe) — final runnable
↓ Loader (OS)
Program runs in RAM
3.5 Script vs Program
Script: A script is a program written in a scripting language that is executed by an interpreter at runtime, usually
without a separate compilation step. Scripts are typically shorter, used for automation and quick tasks, and
executed line by line.
Example: [Link] is a Python script. It runs using the Python interpreter without needing to compile it first to
an .exe file.
Script Compiled Program
Interpreted at runtime Compiled before execution
Usually smaller, task-specific Can be large, complex systems
Examples: Python .py, Bash .sh, JS .js Examples: C .exe, Java .class
Slower (interpreter overhead) Faster (machine code)
Easier to modify and test Need recompile after changes
3.6 Algorithm, Flowchart, Pseudocode, Dry Run
Term Definition Example
Problem Statement A clear description of the problem Find the largest of three numbers.
that needs to be solved.
Algorithm A step-by-step logical solution to a [Link] a,b,c [Link] a>b and a>c: largest=a [Link]...
problem, written in plain language.
Flowchart A diagrammatic (visual) Oval=Start/End, Diamond=Decision,
representation of an algorithm Rectangle=Process
using shapes and arrows.
Pseudocode Writing an algorithm using simple READ a; IF a > 0 THEN PRINT 'Positive'
English-like statements that
resemble code.
Dry Run Manually executing an algorithm Trace through code with specific values to find
step by step on paper to check bugs.
correctness.
Test Case Specific input used to test a Input: 5 Expected Output: 'Positive'
program, along with the expected
output.
Chapter 4: Language Translators
Definition: A language translator is a program that converts source code written in one programming language
into another form (usually machine code) that the computer can execute.
4.1 Compiler
Definition: A compiler is a language translator that reads the entire source program at once, checks it for
errors, and translates it completely into machine code or object code before any execution begins.
▸ Compiler Process
COMPILER WORKFLOW:
Source Code (entire program)
↓
[Lexical Analysis] → Tokenizes all code
↓
[Syntax Analysis] → Checks grammar for entire program
↓
[Semantic Analysis]→ Checks meaning (type checking etc.)
↓
[Code Generation] → Produces Object Code
↓
[Linker] → Combines object files + libraries
↓
Executable (.exe) → Ready to run
✓ All errors reported AFTER the entire program is analysed
✓ Execution is FAST (already machine code)
✓ Examples: GCC (C/C++), javac (Java), go build (Go)
Compiler Feature Detail
Input Entire source code at once
Output Object file / executable
Error reporting All errors shown together after compilation
Execution speed Very fast (pre-compiled machine code)
Separate linking step Yes (linker combines object files)
Examples GCC, G++, javac, [Link], go build
4.2 Interpreter
Definition: An interpreter is a language translator that reads, translates, and executes the source program one
line at a time. It stops as soon as it encounters an error.
▸ Interpreter Process
INTERPRETER WORKFLOW:
Source Code
↓
Read Line 1 → Translate → Execute Line 1
↓
Read Line 2 → Translate → Execute Line 2
↓
Read Line 3 → ERROR FOUND → STOP!
↓
Error reported immediately
✓ No object code is generated
✓ Easier to debug (error found immediately at offending line)
✓ Execution is slower (translation happens at runtime)
✓ Examples: Python, JavaScript (in browser), Ruby
4.3 Assembler
Definition: An assembler is a language translator that converts assembly language (low-level symbolic
instructions like MOV, ADD, JMP) into machine-level binary object code. The output must still be linked to
produce executable machine code.
4.4 Compiler vs Interpreter — Full Comparison
Property Compiler Interpreter
Translation unit Entire program at once One line at a time
Error reporting All errors after full scan Stops at first error
Execution speed Fast (pre-translated) Slower (translates at runtime)
Object code Generates .obj file No intermediate file
Memory usage More (stores whole object code) Less
Debugging Harder (errors listed together) Easier (stops at the error)
Examples C, C++, Java (javac), Go Python, JavaScript, Ruby
Process Compile → Link → Execute Read → Translate → Execute
(repeat)
🔍 Why is Python an Interpreter?
Python reads your source code line by line and executes each line immediately.
HOWEVER: Python also compiles code to bytecode (.pyc files in __pycache__/).
This bytecode is then interpreted by the PVM (Python Virtual Machine).
So Python is technically: compiled to bytecode, then interpreted.
This is why Python is sometimes called a 'compiled-interpreted' language.
Chapter 5: Development Environment
5.1 Code Editor
Definition: A code editor is a software tool specifically designed to write and edit source code. It provides
features that make coding easier compared to a plain text editor.
Feature Description
Syntax Highlighting Different parts of code (keywords, strings, numbers) shown in different
colours.
Auto-Indentation Automatically indents new lines to the correct level.
Code Completion Suggests variable/function names as you type.
Error Highlighting Underlines syntax errors in real time.
Line Numbers Shows line numbers for easy navigation and debugging.
Examples: Notepad++, Sublime Text, VS Code (also an IDE)
5.2 IDE — Integrated Development Environment
Definition: An IDE is a software application that combines multiple development tools into a single graphical
interface to streamline the entire software development workflow.
Tool Built Into IDE Purpose
Code Editor Write and edit source code with syntax highlighting.
Compiler / Interpreter Translate and run code from within the IDE.
Debugger Set breakpoints, step through code, inspect variables at runtime.
Build System Automate compilation and linking steps.
Version Control Integration Git integration — commit, push, pull from within the IDE.
Package Manager Install libraries and dependencies.
Examples: VS Code, PyCharm, Spyder (Anaconda), IDLE (built-in with Python)
5.3 Terminal / Command Line
Definition: A terminal (also called command line interface or CLI) is a text-based interface used to communicate
directly with the operating system by typing commands.
OS Terminal Name How to Open
Windows Command Prompt / Win + R → type 'cmd' → Enter
PowerShell
macOS Terminal Spotlight → type 'Terminal'
Linux Bash / Zsh Ctrl + Alt + T (Ubuntu)
Anaconda Anaconda Prompt Start Menu → Anaconda Prompt
▸ Terminal Commands
# Run a Python file from terminal
python [Link]
# Start Python REPL (interactive mode)
python
# Check Python version
python --version
# Install a package with pip
pip install requests
5.4 IPython — Interactive Python Shell
Definition: IPython (Interactive Python) is an enhanced command-line REPL for Python that provides features
beyond the standard Python interpreter. It is the backbone of Jupyter Notebooks.
Feature Description
Previous output recall _ (last), __ (second last), ___ (third last)
Magic commands Special % commands: %timeit, %run, %pwd, %ls
Tab completion Press Tab to autocomplete variable/function names
? / ?? help obj? shows documentation; obj?? shows source code
Rich output Can display HTML, images, LaTeX in Jupyter
Calculator mode Directly evaluate math expressions
▸ IPython as Calculator
# Open IPython (requires Anaconda or: pip install ipython)
$ ipython
In [1]: 2 + 3
Out[1]: 5
In [2]: 10 * 4
Out[2]: 40
In [3]: _ # last result
Out[3]: 40
In [4]: __ # second last result
Out[4]: 5
In [5]: import math
In [6]: [Link](144)
Out[6]: 12.0
PART 2
PYTHON BASICS — SYNTAX, VARIABLES & I/O
Chapter 6: Core Python Concepts
6.1 Python: A Dynamically Typed Language
Definition: A dynamically typed language is one in which variable data types are determined at runtime — you
do NOT need to declare the type explicitly. A variable's type can even change during execution.
▸ Dynamic Typing
x = 10 # x is int (type decided at runtime)
print(type(x)) # <class 'int'>
x = 'Hello' # x is now str (type CHANGED)
print(type(x)) # <class 'str'>
x = 3.14 # x is now float
print(type(x)) # <class 'float'>
# Contrast with statically typed language (C):
# int x = 10; ← type must be declared, CANNOT change
Property Dynamically Typed (Python) Statically Typed (C, Java)
Type declaration Not needed Required
Type of variable Determined at runtime Determined at compile time
Can type change? Yes No
Error detection Runtime Compile time
Flexibility High Lower
6.2 Programming Paradigms in Python
Paradigm Definition Python Example
Procedural Writing the program step by step, like def add(a,b): return a+b
following a recipe. Uses functions.
Object-Oriented (OOP) Organising code around objects — class Car: def drive(self): ...
entities that combine data and
behaviour.
Functional Treating functions as first-class values. result = list(map(lambda x: x*2,
Avoids changing state. nums))
6.3 Indentation in Python
Definition: Indentation refers to the spaces or tabs at the beginning of a line. In Python, indentation is NOT
optional — it defines code blocks. Python uses indentation instead of curly braces {}. The standard convention is
4 spaces per level.
▸ Indentation Rules
# CORRECT: 4-space indentation
if True:
print('Inside the if block') # 4 spaces = level 1
if True:
print('Nested block') # 8 spaces = level 2
print('Outside all blocks') # 0 spaces = top level
# WRONG: inconsistent indentation → IndentationError
if True:
print('2 spaces') # 2 spaces
print('4 spaces') # 4 spaces ← IndentationError!
# WRONG: mixing tabs and spaces → TabError in Python 3
if True:
print('spaces') # 4 spaces
print('tab') # TAB ← NEVER mix
🔑 Why Does Python Use Indentation?
It forces visual structure to match logical structure.
Every Python program looks similar — reducing cognitive overhead.
Eliminates debates about where to put { } (like in C/Java).
This aligns with the Zen: 'Readability counts.'
Flowchart of indentation-based block:
if condition:
↓ (indent) Code Block A
← (dedent) Code Block B (outside if)
6.4 Comments in Python
Definition: A comment is a part of the source code that the Python interpreter completely ignores during
execution. Comments are written for human readers — to explain code, document logic, or temporarily disable
code during testing.
▸ Comments & Docstrings
# ─── SINGLE-LINE COMMENT ──────────────────────────────
# This entire line is a comment
print('Hello') # Inline comment — after code on same line
# ─── MULTIPLE SINGLE-LINE COMMENTS ───────────────────
# Line 1 of comment
# Line 2 of comment
# Line 3 of comment
# ─── MULTI-LINE COMMENT (using triple quotes) ─────────
"""
This is a multi-line comment.
It spans multiple lines.
Technically this is a string literal that is not assigned,
so Python ignores it. Used as docstrings when placed at
the start of a module/function/class.
"""
# ─── DOCSTRING EXAMPLE ────────────────────────────────
def add(a, b):
"""
Add two numbers and return the result.
Args:
a (int): First number
b (int): Second number
Returns:
int: Sum of a and b
"""
return a + b
print(add.__doc__) # Access the docstring
📌 Single-line (#) vs Multi-line (triple quotes)
# comment → True comment. ALWAYS ignored by Python.
"""...""" → String literal. Ignored only if not assigned to a variable.
When placed first in a function/class/module, it becomes __doc__
Best practice: Use # for inline comments and explanations.
Use triple-quote docstrings to document functions and classes.
6.5 Escape Sequence Characters
Definition: An escape sequence is a backslash (\) followed by a character that represents a special non-
printable or difficult-to-type character inside a string.
Sequence Name Output / Effect Code Example
\n Newline Moves to next line print('Hello\nWorld') → Hello↵World
\t Tab Horizontal tab (8 spaces) print('Name\tAge') → Name Age
\r Carriage Return Moves cursor to start of line print('Hello\rHi') → Hi (overwrites)
\\ Backslash Prints literal backslash print('C:\\Users') → C:\Users
\' Single Quote Prints single quote inside string print('It\'s good') → It's good
\" Double Quote Prints double quote inside print("He said \"Hi\"") → He said "Hi"
string
\b Backspace Removes previous character print('Helloo\b') → Hello
\f Form Feed Page break (mostly legacy) print('Page1\fPage2')
\0 Null Null character (string Not commonly used in Python
terminator)
\ooo Octal Character from octal value print('\101') → A
\xhh Hexadecimal Character from hex value print('\x41') → A
\uXXXX Unicode 16-bit Unicode character print('\u0041') → A
\ Unicode 32-bit Full Unicode character print('\U0001F600') → 😀
UXXXXXXX
X
▸ Escape Sequences
# Escape sequences in practice
print('Hello\nWorld') # Newline
# Hello
# World
print('Name\tAge\tScore') # Tabs for column alignment
# Name Age Score
print('C:\\Users\\Alice') # File path (double backslash)
# C:\Users\Alice
print('It\'s a great day!') # Single quote inside single-quoted string
# It's a great day!
# Raw string — backslash treated literally (no escape processing)
path = r'C:\Users\Alice\Documents'
print(path) # C:\Users\Alice\Documents
regex = r'\d+\.\d+' # Useful for regular expressions
Chapter 7: Variables
Definition (GFG): A variable is a named memory location used to store data values. In Python, a variable is
created the moment you assign a value to it — no declaration keyword is needed. Variables in Python are
references to objects stored in memory, not containers that hold values directly.
▸ Basic Variables
# Variable creation — just assign a value
name = 'Alice' # str variable
age = 25 # int variable
height = 5.6 # float variable
is_active = True # bool variable
score = None # NoneType variable
print(name) # Alice
print(type(name)) # <class 'str'>
print(id(name)) # Memory address (e.g. 140234567890)
7.1 Variable Naming Rules
Rule Valid Examples Invalid Examples
Must start with letter or name, _private, myVar 1name, -var (starts with digit/symbol)
underscore _
Can contain letters, digits, student1, my_var, age_2 my-var, my var (hyphen/space not allowed)
underscores
Case-sensitive name ≠ Name ≠ NAME (all three are different variables!)
Cannot use Python keywords (see keyword list below) if, else, for, while, class, def, etc.
No special characters valid_name my@var, hello!, $price
▸ Python Keywords
# Python keywords — CANNOT be used as variable names
import keyword
print([Link])
# Output (Python 3.12):
# ['False','None','True','and','as','assert','async','await',
# 'break','class','continue','def','del','elif','else',
# 'except','finally','for','from','global','if','import',
# 'in','is','lambda','nonlocal','not','or','pass','raise',
# 'return','try','while','with','yield']
7.2 Naming Conventions (PEP 8)
Convention Style Used For Example
snake_case all_lower_with_underscores Variables, functions student_name,
total_marks,
calculate_area()
PascalCase CapitaliseEveryWord Classes BankAccount,
StudentRecord, Animal
UPPER_CASE ALL_CAPS_WITH_UNDERSCOR Constants PI, MAX_SIZE, GRAVITY
ES
_single_leading _name Protected attribute _balance, _password
(convention only)
__double_leading __name Private attribute (name- __secret, __id
mangled by Python)
__dunder__ __name__ Special/magic methods __init__, __str__, __len__
7.3 Assigning Variables
▸ Assignment Types
# ── Single assignment ───────────────────────────────────
x = 10
name = 'Alice'
# ── Multiple assignment in one line (left to right) ────
x, y, z = 1, 2, 3
print(x, y, z) # 1 2 3
# ── Assign same value to multiple variables ─────────────
a = b = c = 0
print(a, b, c) # 0 0 0
# ── Swap two variables (Pythonic) ───────────────────────
# Traditional method (using temp variable)
a, b = 5, 10
temp = a
a = b
b = temp
print(a, b) # 10 5
# Python shortcut (tuple packing/unpacking)
a, b = 5, 10
a, b = b, a # Right side evaluated first
print(a, b) # 10 5
# ── Augmented assignment operators ──────────────────────
x = 10
x += 5 # x = x + 5 = 15
x -= 3 # x = x - 3 = 12
x *= 2 # x = x * 2 = 24
x /= 4 # x = x / 4 = 6.0
x //= 2 # x = x // 2 = 3 (floor division)
x **= 3 # x = x ** 3 = 27 (exponentiation)
x %= 5 # x = x % 5 = 2 (modulo)
7.4 Variable Scope — Global & Local
Definition: Scope determines where a variable is accessible in your code. Python has two main scope types:
Scope Type Definition Where Accessible
Global Variable Created OUTSIDE any function. Anywhere in the module — inside and
outside functions.
Local Variable Created INSIDE a function. Only within that specific function.
Dies when function returns.
▸ Scope Examples
# ── GLOBAL variable ─────────────────────────────────────
x = 'global' # Created at module level
def show():
print(x) # Can READ global x
show() # Output: global
print(x) # Output: global
# ── LOCAL variable ──────────────────────────────────────
def create_local():
y = 'local' # Only exists inside this function
print(y) # Output: local
create_local() # Output: local
# print(y) # NameError! y does not exist here
# ── SAME NAME: local shadows global ─────────────────────
x = 'global'
def shadow():
x = 'local' # New LOCAL x, doesn't affect global
print(x) # Output: local
shadow() # Output: local
print(x) # Output: global (unchanged!)
▸ global Keyword
# ── The 'global' keyword ────────────────────────────────
counter = 0 # global
def increment():
global counter # Tell Python: use the GLOBAL counter
counter += 1 # Now modifying global variable
increment()
increment()
print(counter) # 2
# ── Without 'global' keyword → UnboundLocalError ────────
count = 0
def bad_increment():
count += 1 # UnboundLocalError!
# Python sees 'count =' so treats count as local.
# But count isn't defined locally yet. Error!
7.5 Variable Memory Model
Key Concept: In Python, variables are NOT containers that hold values. They are labels/references that point to
objects stored in memory (the heap). This distinction is critical for understanding mutation and assignment.
▸ Memory Model
# MEMORY DIAGRAM
x = 10
# Stack: x ──────→ Heap: [int object: value=10, id=0x1A, ref_count=1]
y = x
# Stack: x ──┐
# y ──┘──→ Heap: [int object: value=10, id=0x1A, ref_count=2]
# BOTH x and y point to the SAME int object!
print(id(x) == id(y)) # True — same memory address
y = 20
# Stack: x ──────→ Heap: [int object: value=10, id=0x1A, ref_count=1]
# y ──────→ Heap: [int object: value=20, id=0x2B, ref_count=1]
# Now x and y point to DIFFERENT objects
# Check identity vs equality
a = [1, 2, 3]
b = a # b points to SAME list as a
c = [1, 2, 3] # c points to a DIFFERENT list with same values
print(a == b) # True — same values
print(a is b) # True — same object (same id)
print(a == c) # True — same values
print(a is c) # False — different objects (different id)
7.6 Constants in Python
Definition: Python does NOT have a built-in constant type (unlike C's const or Java's final). By convention,
constants are written in ALL_CAPS with underscores. The programmer is trusted not to modify them.
▸ Constants
# Constants by convention — ALL UPPERCASE
PI = 3.14159265358979
GRAVITY = 9.8
MAX_SIZE = 100
BASE_URL = '[Link]
DEBUG = False
# Python 3.8+ — you can use Final for type-checking enforcement
from typing import Final
MAX_CONNECTIONS: Final = 100
# Type checkers (mypy) will warn if you try to reassign MAX_CONNECTIONS
7.7 Deleting Variables
▸ Deleting Variables
x = 10
print(x) # 10
del x # Delete the variable (unbind name from object)
# print(x) # NameError: name 'x' is not defined
# Delete multiple variables at once
a, b, c = 1, 2, 3
del a, b # a and b deleted; c still exists
print(c) # 3
# Note: del removes the NAME binding, not necessarily the object.
# The object is garbage collected when ref count reaches 0.
7.8 Checking Variable Type
▸ type() and isinstance()
# Use type() function to find the type of any variable
a = 'Hello World'
b = 10
c = 11.22
d = True
e = [1, 2, 3]
f = {'key': 'value'}
g = None
print(type(a)) # <class 'str'>
print(type(b)) # <class 'int'>
print(type(c)) # <class 'float'>
print(type(d)) # <class 'bool'>
print(type(e)) # <class 'list'>
print(type(f)) # <class 'dict'>
print(type(g)) # <class 'NoneType'>
# isinstance() — checks if variable is a specific type (or subclass)
print(isinstance(b, int)) # True
print(isinstance(b, (int, float))) # True — check multiple types
print(isinstance(True, int)) # True — bool IS a subclass of int!
Chapter 8: Input and Output
8.1 Output with print()
Definition: The print() function outputs data to standard output (usually the screen). It can accept multiple
arguments, and provides parameters to control formatting.
▸ print() Function — All Features
# FULL SIGNATURE:
# print(*objects, sep=' ', end='\n', file=[Link], flush=False)
# ── Basic printing ──────────────────────────────────────
print('Hello World') # Hello World
print(42) # 42 (no quotes needed for numbers)
print(3.14) # 3.14
print(True) # True
# ── Printing multiple values ────────────────────────────
x, y = 10, 20
print(x, y) # 10 20 (space between by default)
print('x =', x, 'and y =', y) # x = 10 and y = 20
# ── sep parameter — custom separator ────────────────────
print(1, 2, 3) # 1 2 3 (default sep=' ')
print(1, 2, 3, sep=', ') # 1, 2, 3
print(1, 2, 3, sep=' | ') # 1 | 2 | 3
print(1, 2, 3, sep='') # 123 (no separator)
print('2025', '01', '30', sep='-') # 2025-01-30
# ── end parameter — control line ending ─────────────────
print('Hello', end=' ') # Hello (space instead of newline)
print('World') # World
# Combined output: Hello World (on same line)
# ── Print to file ───────────────────────────────────────
import sys
print('Error message!', file=[Link]) # Print to stderr
with open('[Link]', 'w') as f:
print('Written to file!', file=f) # Print to file
# ── Math inside print ───────────────────────────────────
print(3 + 3) # 6
print(2 ** 10) # 1024
print(10 / 3) # 3.3333333333333335
Parameter Type Default Description
*objects any (required) Any number of values to print. Converted to str
automatically.
sep str ' ' (space) Separator inserted between multiple objects.
end str '\n' (newline) String appended after the last object.
file file object [Link] File-like object to write to.
flush bool False If True, output buffer is forcefully flushed
immediately.
8.2 Output Formatting Methods
No Method Code Example Output
.
1 Default print() print('Hello','World') Hello World
2 sep parameter print('A','B','C',sep='-') A-B-C
3 Space in string print('Hello World') Hello World
4 Concatenation print('Hello'+' '+'World') Hello World
5 end parameter print('Hello',end=' '); print('World') Hello World
6 f-string print(f'{a} {b}') value1 value2
7 String * n print(' '*4 + 'Hi') Hi
8 Tab \t print('Name\tAge') Name Age
9 Newline \n print('Hello\nWorld') Hello (newline) World
10 .format() print('{} {}'.format('Hi','There')) Hi There
11 join() print(' '.join(['A','B','C'])) ABC
12 %-formatting print('Name: %s, Age: %d' % ('Ali',25)) Name: Ali, Age: 25
8.3 f-Strings (Formatted String Literals)
Definition: An f-string (formatted string literal) is a way to embed expressions directly inside string literals using
curly braces {...}. Introduced in Python 3.6 (PEP 498). It is the recommended modern way to format strings in
Python.
▸ f-Strings — Complete Guide
# SYNTAX: f'...{expression}...' or f"...{expression}..."
# ── Basic usage ─────────────────────────────────────────
name = 'Harsh'
age = 21
print(f'My name is {name} and my age is {age}')
# Output: My name is Harsh and my age is 21
# ── Expressions inside f-strings ────────────────────────
a, b = 10, 5
print(f'Sum = {a + b}') # Sum = 15
print(f'Product = {a * b}') # Product = 50
print(f'Double of a = {a * 2}') # Double of a = 20
print(f'Is a > b? {a > b}') # Is a > b? True
# ── Method calls inside f-strings ───────────────────────
word = 'python'
print(f'Upper: {[Link]()}') # Upper: PYTHON
print(f'Length: {len(word)}') # Length: 6
# ── Number formatting ────────────────────────────────────
pi = 3.14159265
print(f'Pi = {pi:.2f}') # Pi = 3.14 (2 decimal places)
print(f'Pi = {pi:.4f}') # Pi = 3.1416
print(f'Large = {1000000:,}') # Large = 1,000,000 (comma separator)
print(f'Hex = {255:#x}') # Hex = 0xff
print(f'Binary = {10:b}') # Binary = 1010
print(f'Sci = {0.000123:.2e}') # Sci = 1.23e-04
# ── Alignment with f-strings ─────────────────────────────
# Syntax: {value:alignment width}
# < = left align > = right align ^ = center
print(f"{'Hi':<10}END") # Hi END (left, width 10)
print(f"{'Hi':>10}END") # HiEND (right, width 10)
print(f"{'Hi':^10}END") # Hi END (center, width 10)
print(f"{'Hi':*^10}END") # ****Hi****END (fill with *)
# ── Tabular output with f-strings ───────────────────────
print(f"{'Name':<15}{'Marks':>10}{'Grade':>8}")
print(f"{'Harsh':<15}{95:>10}{'A':>8}")
print(f"{'Priya':<15}{88:>10}{'B':>8}")
# Name Marks Grade
# Harsh 95 A
# Priya 88 B
Format Spec Meaning Example Output
:.2f Float with 2 decimal f'{3.14159:.2f}' 3.14
places
:, Thousand separator f'{1000000:,}' 1,000,000
:b Binary representation f'{10:b}' 1010
:x Hexadecimal (lowercase) f'{255:x}' ff
:o Octal representation f'{8:o}' 10
:e Scientific notation f'{12345.678:.2e}' 1.23e+04
:<10 Left-align in width 10 f"{'Hi':<10}|" Hi |
:>10 Right-align in width 10 f"{'Hi':>10}|" Hi|
:^10 Center in width 10 f"{'Hi':^10}|" Hi |
:0>5 Right-align, pad with 0 f'{42:0>5}' 00042
8.4 Input with input()
Definition: The input() function reads a line of text from the user and always returns it as a string, regardless
of what the user types. You must explicitly convert the type if you need a number.
▸ input() — Complete Guide
# SYNTAX: input(prompt_string) → str
# ── Basic input ─────────────────────────────────────────
name = input('Enter your name: ') # Returns str
print('Hello,', name)
# ── Type conversion (typecasting) ───────────────────────
# input() ALWAYS returns str — convert when needed
age = int(input('Enter age: ')) # str → int
height = float(input('Enter height: ')) # str → float
# ── Multiple inputs on one line using split() ────────────
x, y = input('Enter two numbers: ').split()
# User types: 5 10
# x = '5' (str) y = '10' (str) — still strings!
print('x =', x, 'y =', y)
# ── Multiple inputs with type conversion (map) ───────────
x, y = map(int, input('Enter two integers: ').split())
# User types: 5 10
# x = 5 (int) y = 10 (int)
print(f'Sum = {x + y}') # Sum = 15
# map(function, iterable)
# → Applies 'int' to each element produced by split()
# ── Three values ─────────────────────────────────────────
x, y, z = map(int, input('Enter three numbers: ').split())
# User types: 5 10 15
print(f'Total = {x + y + z}') # Total = 30
# ── Input list of integers ───────────────────────────────
nums = list(map(int, input('Enter numbers: ').split()))
# User types: 1 2 3 4 5
print(nums) # [1, 2, 3, 4, 5]
print(sum(nums)) # 15
⚠️ Common Mistake: Forgetting Type Conversion
❌ WRONG:
age = input('Enter age: ') # age is '25' (str)
new_age = age + 5 # TypeError: can't add str and int
✅ CORRECT:
age = int(input('Enter age: ')) # age is 25 (int)
new_age = age + 5 # 30 ✓
PART 3
DATA TYPES — COMPLETE REFERENCE
Chapter 9: Python Data Types Overview
Definition (GFG): Data types classify data items and determine the kind of value a variable can hold and what
operations can be performed on it. In Python, every value is an object, and every object has a type. Python data
types are implemented as classes, and variables are instances of these classes.
Category Types Example Values
Text Type str 'hello', "Python", '''multiline'''
Numeric Types int, float, complex 42, 3.14, 2+3j
Sequence Types list, tuple, range [1,2,3], (1,2), range(10)
Mapping Type dict {"key": "value"}
Set Types set, frozenset {1,2,3}, frozenset({1,2})
Boolean Type bool True, False
Binary Types bytes, bytearray, memoryview b'hello', bytearray(5)
None Type NoneType None
▸ type() on All Data Types
# Check any value's data type with type()
print(type(42)) # <class 'int'>
print(type(3.14)) # <class 'float'>
print(type('hello')) # <class 'str'>
print(type(True)) # <class 'bool'>
print(type([1,2,3])) # <class 'list'>
print(type((1,2,3))) # <class 'tuple'>
print(type({1,2,3})) # <class 'set'>
print(type({'a':1})) # <class 'dict'>
print(type(None)) # <class 'NoneType'>
print(type(1+2j)) # <class 'complex'>
print(type(range(5))) # <class 'range'>
Chapter 10: Numeric Types
10.1 Integer (int)
Definition: An integer is a whole number (positive, negative, or zero) without any decimal point. Python
integers have unlimited precision — they can be arbitrarily large (limited only by available memory). This is
unlike C/Java where int has a fixed size.
▸ Integer (int)
# ── Creating integers ───────────────────────────────────
a = 10
b = -250
c = 0
huge = 99999999999999999999999999999999 # No overflow!
# ── Integer literals in different bases ─────────────────
decimal = 255 # Base 10 (default) → 255
binary = 0b11111111 # Base 2 (prefix 0b) → 255
octal = 0o377 # Base 8 (prefix 0o) → 255
hexadec = 0xFF # Base 16 (prefix 0x) → 255
print(binary, octal, hexadec) # 255 255 255
# ── Readability: underscores as thousand separators ─────
million = 1_000_000 # Python 3.6+
pi_6dp = 3_141_593
credit = 1_234_567_890
# ── Converting between bases ─────────────────────────────
print(bin(255)) # '0b11111111' (int → binary string)
print(oct(255)) # '0o377' (int → octal string)
print(hex(255)) # '0xff' (int → hex string)
print(int('ff', 16)) # 255 (hex string → int)
print(int('11111111', 2)) # 255 (binary string → int)
print(int('377', 8)) # 255 (octal string → int)
# ── Arithmetic ───────────────────────────────────────────
print(10 + 3) # 13 addition
print(10 - 3) # 7 subtraction
print(10 * 3) # 30 multiplication
print(10 / 3) # 3.3333... TRUE division (always float)
print(10 // 3) # 3 FLOOR division
print(10 % 3) # 1 modulo (remainder)
print(2 ** 10) # 1024 exponentiation
⚙ CPython Integer Caching (-5 to 256)
CPython caches small integers from -5 to 256.
Any variable assigned a value in this range refers to the SAME object.
>>> a = 256; b = 256
>>> a is b # True — same cached object
>>> a = 257; b = 257
>>> a is b # False — different objects (outside cache)
Rule: ALWAYS use == for value comparison. Use 'is' ONLY for None checks.
10.2 Float
Definition: A float (floating-point number) is a number that contains a decimal point. Python floats are
implemented as 64-bit IEEE 754 double-precision values, giving about 15-17 significant decimal digits of
precision.
▸ Float (float)
# ── Creating floats ─────────────────────────────────────
a = 3.14
b = -2.718
c = 0.0
d = 1.5e10 # Scientific notation: 1.5 × 10¹⁰
e = 2.5e-4 # 2.5 × 10⁻⁴ = 0.00025
# ── The floating-point precision problem ─────────────────
print(0.1 + 0.2) # 0.30000000000000004 ← NOT 0.3!
print(0.1 + 0.2 == 0.3) # False!
# Root cause: 0.1 and 0.2 cannot be represented EXACTLY
# in binary floating-point (IEEE 754)
# ── Solutions ────────────────────────────────────────────
# Solution 1: round() for display
print(round(0.1 + 0.2, 1)) # 0.3
# Solution 2: [Link]() for comparison
import math
print([Link](0.1+0.2, 0.3)) # True
# Solution 3: decimal module for exact arithmetic
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2')) # 0.3 (exact!)
# ── Special float values ──────────────────────────────────
print([Link]) # inf (infinity)
print(-[Link]) # -inf (negative infinity)
print([Link]) # nan (Not a Number)
print(1.0 / 0) # ZeroDivisionError
print(float('inf')) # inf
print([Link]([Link])) # True
print([Link]([Link])) # True
# ── Float info ──────────────────────────────────────────
import sys
print(sys.float_info.max) # 1.7976931348623157e+308
print(sys.float_info.min) # 2.2250738585072014e-308
10.3 Complex
Definition: A complex number has a real part and an imaginary part. In Python, the imaginary part is written
with a j suffix (not i as in mathematics).
▸ Complex Numbers
# Creating complex numbers
c1 = 3 + 4j # real=3, imaginary=4
c2 = -2 - 7j
c3 = complex(5, 3) # complex(real, imag) → 5+3j
print([Link]) # 3.0 — always float
print([Link]) # 4.0 — always float
print(abs(c1)) # 5.0 — magnitude: √(3²+4²) = 5
# Arithmetic with complex
print(c1 + c2) # (1-3j)
print(c1 * c2) # (22-29j)
print([Link]()) # (3-4j)
10.4 Boolean (bool)
Definition: The boolean type has only two values: True and False. In Python, bool is a subclass of int. True
equals 1 and False equals 0. This means you can perform arithmetic operations with booleans.
▸ Boolean (bool)
# bool is a subclass of int
print(isinstance(True, int)) # True!
print(True == 1) # True
print(False == 0) # True
# Arithmetic with booleans
print(True + True) # 2 (1 + 1)
print(True * 5) # 5
print(False + 1) # 1
# Practical: count True values
flags = [True, False, True, True, False, True]
print(sum(flags)) # 4 (counts Trues)
# ── Truthy and Falsy values ──────────────────────────────
# FALSY (evaluate to False in boolean context):
# False, None, 0, 0.0, 0j, '', [], (), {}, set(), range(0)
# TRUTHY (evaluate to True):
# Everything else — non-zero numbers, non-empty containers
# Examples
print(bool(0)) # False
print(bool(1)) # True
print(bool('')) # False (empty string)
print(bool('hi')) # True (non-empty string)
print(bool([])) # False (empty list)
print(bool([0])) # True (list with one item, even if item is falsy!)
print(bool(None)) # False
# Use in conditions
name = input('Name: ')
if name: # equivalent to: if name != ''
print(f'Hello, {name}')
else:
print('No name provided')
10.5 Type Conversion
Definition: Type conversion is changing a value from one data type to another. Python supports two kinds:
implicit (done automatically by Python) and explicit (done manually by the programmer using built-in functions
— also called typecasting).
▸ Type Conversion
# ── IMPLICIT conversion (Python does it automatically) ──
# Python promotes to the 'wider' type to avoid data loss
x = 10 # int
y = 3.5 # float
result = x + y # Python converts x to float automatically
print(result) # 13.5
print(type(result)) # <class 'float'>
# bool + int → int
print(True + 5) # 6 (True treated as 1)
# ── EXPLICIT conversion (typecasting) ───────────────────
# int() → integer
print(int(3.9)) # 3 (truncates, does NOT round!)
print(int('42')) # 42
print(int(True)) # 1
print(int('0b1010', 0)) # 10 (binary string)
print(int('0xFF', 0)) # 255 (hex string)
# float() → float
print(float(5)) # 5.0
print(float('3.14')) # 3.14
print(float(True)) # 1.0
# str() → string
print(str(42)) # '42'
print(str(3.14)) # '3.14'
print(str(True)) # 'True'
print(str([1,2,3])) # '[1, 2, 3]'
# bool() → boolean
print(bool(0)) # False
print(bool(42)) # True
print(bool('')) # False
print(bool('hello')) # True
# list(), tuple(), set() → conversions between sequences
s = 'Python'
print(list(s)) # ['P','y','t','h','o','n']
print(tuple(s)) # ('P','y','t','h','o','n')
print(set(s)) # {'y','h','o','t','P','n'} (unordered, unique)
Function Converts To Notes
int(x) Integer Truncates floats. Parses strings if they are integers.
float(x) Float Accepts 'inf' and 'nan' as strings too.
str(x) String Works on any type.
bool(x) Boolean See truthy/falsy rules.
list(x) List Works on any iterable.
tuple(x) Tuple Works on any iterable.
set(x) Set Removes duplicates. Works on any iterable.
dict(x) Dictionary Requires key-value pairs (list of 2-tuples).
complex(r, i) Complex complex(3, 4) → 3+4j
chr(n) Character chr(65) → 'A' (int → Unicode char)
ord(c) Integer ord('A') → 65 (char → Unicode code point)
10.6 The random Module
Definition: Python's built-in random module provides functions to generate random numbers. Python does not
have a standalone random() built-in function — you must import the module.
▸ random Module
import random
# ── Random integers ─────────────────────────────────────
print([Link](1, 10)) # Random int from 1 to 10 (inclusive)
print([Link](0, 100, 5)) # Random from 0,5,10,...,95
# ── Random floats ───────────────────────────────────────
print([Link]()) # Float from [0.0, 1.0)
print([Link](1.5, 10.0)) # Float from [1.5, 10.0]
# ── Random from sequences ───────────────────────────────
fruits = ['apple', 'banana', 'cherry', 'date']
print([Link](fruits)) # One random element
print([Link](fruits, k=2)) # k random elements (with replacement)
print([Link](fruits, k=2)) # k random elements (without replacement)
# ── Shuffle ─────────────────────────────────────────────
nums = [1, 2, 3, 4, 5]
[Link](nums) # Shuffle IN PLACE
print(nums) # e.g. [3,1,5,2,4]
# ── Reproducible randomness with seed ───────────────────
[Link](42) # Fix the random sequence
print([Link]()) # Always same value for same seed
Method Description Example
[Link]() Random float [0.0, 1.0) 0.37444887175646646
[Link](a,b) Random int [a, b] inclusive [Link](1,6) → dice roll
[Link](start,stop, Random from range [Link](0,100,2) → even
step) num
[Link](a,b) Random float [a, b] [Link](0,1) → 0.534...
[Link](seq) Random element from sequence [Link](['a','b','c']) → 'b'
[Link](seq,k=n) n random elements (with [Link]([1,2,3],k=5)
replacement)
[Link](seq,k=n) n unique random elements [Link](range(100),k=5)
[Link](lst) Shuffle list in-place [Link]([1,2,3,4])
[Link](n) Set seed for reproducibility [Link](42)
[Link](n) n random bits as integer [Link](8) → 0-255
PART 4
STRINGS — COMPLETE DEEP DIVE
Chapter 11: Strings
Definition (GFG): A string is an ordered, immutable sequence of Unicode characters enclosed in quotes. In
Python, strings are objects of the str class. Unlike C, Python has no separate char type — a single character is
simply a string of length 1.
11.1 Creating Strings
▸ Creating Strings
# ── All ways to create strings ──────────────────────────
s1 = 'Single quotes'
s2 = "Double quotes"
s3 = '''Triple single — can span
multiple lines'''
s4 = """Triple double — also
multi-line"""
# ── Using quotes inside strings ──────────────────────────
print("It's alright") # Single quote inside double
print('He said "Hello"') # Double quote inside single
print('He is called \'Johnny\'') # Escaped single quote
# ── Raw strings (r prefix) — backslash is literal ───────
path = r'C:\Users\Alice\Documents' # No escape processing
regex = r'\d+\.\d+' # Regex pattern
print(path) # C:\Users\Alice\Documents
# ── Byte strings (b prefix) ──────────────────────────────
bs = b'Hello bytes' # bytes object, not str
print(type(bs)) # <class 'bytes'>
11.2 Strings are Arrays — Indexing
Definition: A string behaves like an array (sequence) of characters. Each character has a position called an
index. Forward indices start at 0; backward (negative) indices start at -1 from the end.
▸ String Indexing
# String: P y t h o n
# Forward: 0 1 2 3 4 5
# Backward:-6 -5 -4 -3 -2 -1
s = 'Python'
# ── Indexing ─────────────────────────────────────────────
print(s[0]) # 'P' — first character
print(s[1]) # 'y'
print(s[-1]) # 'n' — last character
print(s[-2]) # 'o' — second from end
# A single character is still a str (not a 'char' type)
letter = s[0]
print(type(letter)) # <class 'str'>
print(len(letter)) # 1
# ── IndexError if out of range ───────────────────────────
# print(s[10]) → IndexError: string index out of range
11.3 String Slicing
Syntax: string[start : stop : step] — Extract a substring. stop is EXCLUDED. Omitting start defaults
to 0; omitting stop defaults to end; omitting step defaults to 1.
▸ Slicing
s = 'Python Programming'
# Index: 0123456789...
# ── Basic slices ─────────────────────────────────────────
print(s[0:6]) # 'Python' (indices 0..5)
print(s[7:]) # 'Programming' (from 7 to end)
print(s[:6]) # 'Python' (from start to 5)
print(s[:]) # 'Python Programming' (full copy)
# ── Step slicing ─────────────────────────────────────────
print(s[::2]) # 'Pto rgamn' (every 2nd character)
print(s[::3]) # 'Ph rai' (every 3rd)
# ── REVERSE a string — most common trick ─────────────────
print(s[::-1]) # 'gnimmargorP nohtyP'
# ── Negative indices in slices ───────────────────────────
print(s[-11:]) # 'Programming'
print(s[:-12]) # 'Python'
print(s[-4:]) # 'ming'
# ── Extracting every other character from 2nd half ───────
print(s[7::2]) # 'Pormig'
# ── Empty slice if start > stop ──────────────────────────
print(s[5:2]) # '' (empty — cannot go backward with positive step)
print(s[5:2:-1]) # 'noh' (go backward with step -1)
11.4 String Immutability
Definition: Strings in Python are immutable — once created, their characters cannot be changed. Any operation
that appears to modify a string actually creates a NEW string.
▸ Immutability
s = 'Hello'
# Cannot change individual characters
# s[0] = 'J' → TypeError: 'str' object does not support item assignment
# To 'modify' a string, create a new one
s = 'J' + s[1:] # New string: 'Jello'
print(s) # Jello
# String operations always return NEW strings
s = 'hello'
upper_s = [Link]() # 'HELLO' — new string
print(s) # 'hello' — original UNCHANGED!
print(upper_s) # 'HELLO'
# Memory efficiency: avoid += in a loop
# BAD (creates many intermediate strings):
result = ''
for i in range(1000):
result += str(i) # Creates new str every iteration!
# GOOD (use join):
result = ''.join(str(i) for i in range(1000)) # Efficient
11.5 String Methods — Complete Reference
Method Description Example → Output
upper() Converts all to uppercase 'hello'.upper() → 'HELLO'
lower() Converts all to lowercase 'HELLO'.lower() → 'hello'
capitalize() First char uppercase, rest lowercase 'hELLO'.capitalize() → 'Hello'
title() First char of each word uppercase 'hello world'.title() → 'Hello World'
swapcase() Swaps upper↔lower 'Hello'.swapcase() → 'hELLO'
strip() Removes leading/trailing ' hi '.strip() → 'hi'
whitespace
lstrip() Removes leading whitespace ' hi '.lstrip() → 'hi '
rstrip() Removes trailing whitespace ' hi '.rstrip() → ' hi'
strip(chars) Removes specified leading/trailing '***hi***'.strip('*') → 'hi'
chars
split(sep) Splits into list on separator 'a,b,c'.split(',') → ['a','b','c']
split() Splits on whitespace 'a b c'.split() → ['a','b','c']
rsplit(sep,n) Splits from right, max n splits 'a.b.c'.rsplit('.',1) → ['a.b','c']
join(iterable) Joins iterable with string as ','.join(['a','b','c']) → 'a,b,c'
separator
replace(old,new) Replaces occurrences 'cat'.replace('c','b') → 'bat'
replace(old,new,n) Replaces first n occurrences 'aaa'.replace('a','b',2) → 'bba'
find(sub) Index of first occurrence (-1 if 'hello'.find('ll') → 2
absent)
rfind(sub) Index of last occurrence 'hello'.rfind('l') → 3
index(sub) Like find() but raises ValueError if 'hello'.index('e') → 1
absent
count(sub) Count non-overlapping occurrences 'banana'.count('a') → 3
startswith(prefix) True if starts with prefix 'hello'.startswith('he') → True
endswith(suffix) True if ends with suffix 'hello'.endswith('lo') → True
isdigit() True if all chars are digits '123'.isdigit() → True
isalpha() True if all chars are letters 'abc'.isalpha() → True
isalnum() True if all alphanumeric 'abc123'.isalnum() → True
isspace() True if all whitespace ' '.isspace() → True
isupper() True if all uppercase 'HELLO'.isupper() → True
islower() True if all lowercase 'hello'.islower() → True
istitle() True if title case 'Hello World'.istitle() → True
zfill(width) Pad with leading zeros '42'.zfill(5) → '00042'
center(w,fill) Center in width w 'hi'.center(7,'*') → '**hi***'
ljust(w,fill) Left-justify in width w 'hi'.ljust(7,'-') → 'hi-----'
rjust(w,fill) Right-justify in width w 'hi'.rjust(7,'-') → '-----hi'
encode(enc) Encode string to bytes 'hello'.encode('utf-8') → b'hello'
format(**kwargs) String interpolation '{name}'.format(name='Ali') → 'Ali'
expandtabs(n) Expand tabs to spaces 'a\tb'.expandtabs(4) → 'a b'
partition(sep) Split into 3-tuple at first sep 'a:b:c'.partition(':') → ('a',':','b:c')
removeprefix(pre) Remove prefix if present (Py 3.9+) 'TestHello'.removeprefix('Test') →
'Hello'
removesuffix(suf) Remove suffix if present (Py 3.9+) 'HelloTest'.removesuffix('Test') → 'Hello'
11.6 Looping Through Strings
Definition: Since a string is a sequence of characters, you can iterate over it using loops. Python provides
multiple ways to loop through a string.
▸ Looping Through Strings — All 8 Methods
# ── Method 1: Simple for loop (most Pythonic) ───────────
word = 'Python'
for letter in word:
print(letter) # Prints: P y t h o n (one per line)
# ── Method 2: Using range() and index ───────────────────
for i in range(len(word)):
print(word[i]) # Same output via index access
# ── Method 3: while loop ─────────────────────────────────
i = 0
while i < len(word):
print(word[i])
i += 1
# ── Method 4: Print on same line (end=' ') ───────────────
for letter in word:
print(letter, end=' ') # P y t h o n
print() # newline after
# ── Method 5: Using enumerate() — index + value ──────────
for index, letter in enumerate(word):
print(f'{index}: {letter}')
# 0: P 1: y 2: t 3: h 4: o 5: n
# ── Method 6: Reverse loop ───────────────────────────────
for i in range(len(word)-1, -1, -1):
print(word[i], end='') # nohtyP
# Or simpler:
print(word[::-1]) # nohtyP
# ── Method 7: Loop and count (e.g., vowels) ─────────────
word = 'Python Programming'
vowels = 'aeiouAEIOU'
count = 0
for letter in word:
if letter in vowels:
count += 1
print(f'Vowels: {count}') # Vowels: 5
# ── Method 8: Loop with condition — print only vowels ────
for letter in word:
if letter in 'aeiou':
print(letter, end=' ') # o o a i
11.7 enumerate() Deep Dive
Definition (GFG): enumerate() is a built-in function that adds a counter to an iterable and returns an
enumerate object. It produces pairs of (index, element), eliminating the need to manually maintain a counter
variable.
▸ enumerate()
# SYNTAX: enumerate(iterable, start=0)
# Returns: enumerate object → yields (index, element) pairs
# ── Basic usage ─────────────────────────────────────────
fruits = ['apple', 'banana', 'cherry']
for i, fruit in enumerate(fruits):
print(i, fruit)
# 0 apple
# 1 banana
# 2 cherry
# ── Custom start index ───────────────────────────────────
for i, fruit in enumerate(fruits, start=1):
print(f'{i}. {fruit}')
# 1. apple
# 2. banana
# 3. cherry
# ── With strings ─────────────────────────────────────────
word = 'Python'
for index, letter in enumerate(word):
print(f'Position {index}: {letter}')
# ── Convert to list of tuples ────────────────────────────
result = list(enumerate(['a','b','c']))
print(result) # [(0,'a'), (1,'b'), (2,'c')]
# ── Practical: find index of an element ──────────────────
items = ['pen', 'book', 'pencil', 'book']
for i, item in enumerate(items):
if item == 'book':
print(f'book found at index {i}')
# book found at index 1
# book found at index 3
11.8 String Formatting: % Style and .format()
▸ String Formatting Styles
# ── % formatting (old style, still widely seen) ──────────
name = 'Alice'
age = 25
print('Name: %s, Age: %d' % (name, age))
# Name: Alice, Age: 25
# Format codes:
# %s → string %d → integer %f → float
# %e → scientific %o → octal %x → hex %% → literal %
print('Pi = %.2f' % 3.14159) # Pi = 3.14
print('Score: %05d' % 42) # Score: 00042
# ── .format() method (Python 3, pre f-strings) ──────────
print('Name: {}, Age: {}'.format(name, age))
print('Name: {0}, Age: {1}'.format(name, age)) # by index
print('Name: {n}, Age: {a}'.format(n=name, a=age)) # by name
# Alignment with .format()
print('{:<15}{:>10}'.format('Product', 'Price'))
print('{:<15}{:>10.2f}'.format('Apple', 1.5))
# Product Price
# Apple 1.50
# ── Template strings ([Link]) ───────────────────
from string import Template
t = Template('Hello, $name! You are $age years old.')
print([Link](name='Bob', age=30))
# Hello, Bob! You are 30 years old.
# Safe substitute: doesn't raise error for missing keys
print(t.safe_substitute(name='Bob'))
# Hello, Bob! You are $age years old.
PART 5
OPERATORS — COMPLETE REFERENCE
Chapter 12: Python Operators
Definition (GFG): Operators are special symbols or keywords that perform operations on values and variables
called operands. Python operators are divided into 8 categories. Every Python expression uses at least one
operator — from the simplest assignment to complex logical conditions.
💡 Why Operators Matter
Operators are the foundation of every computation in programming.
Without operators, programs cannot do math, make decisions, or compare values.
Python has a rich set of operators with clear precedence rules.
Understanding operator behavior prevents subtle bugs (e.g., // vs /, is vs ==).
12.1 Arithmetic Operators
Arithmetic operators perform mathematical operations. Python supports all standard math operations plus two
extras: floor division (//) and exponentiation (**).
Operator Name Example Result Important Note
+ Addition 10 + 3 13 Works on strings too: 'a'+'b'='ab'
- Subtraction 10 - 3 7 Works with negative numbers
* Multiplication 10 * 3 30 Works on strings: 'ab'*3='ababab'
/ True Division 10 / 3 3.3333… ALWAYS returns float, even 4/2=2.0
// Floor Division 10 // 3 3 Rounds DOWN (toward -infinity)
% Modulus 10 % 3 1 Remainder after division; useful for
even/odd
** Exponentiation 2 ** 10 1024 Right-to-left associative: 2**3**2 = 512
▸ Arithmetic Operators — Deep Dive
# Arithmetic deep dive
print(10 / 3) # 3.3333333333333335 (always float!)
print(10 // 3) # 3 — floor division (integer result)
print(-7 // 2) # -4 — floors toward -infinity, NOT -3!
print(7 // -2) # -4 — same rule
print(10 % 3) # 1 — 10 = 3*3 + 1
print(-10 % 3) # 2 — Python modulo always non-negative when divisor > 0
print(2 ** 3) # 8
print(2 ** 0.5) # 1.4142... (square root via exponent!)
# String repetition with * and +
print('Ha' * 3) # HaHaHa
print('Hi' + ' ' + 'World') # Hi World
# Practical examples
total = 1234
hundreds = total // 100 # 12
remainder = total % 100 # 34
is_even = total % 2 == 0 # False
print(hundreds, remainder, is_even)
3.3333333333333335 3 -4 -4 1 2 8 1.4142135623730951 HaHaHa Hi World 12 34
False
12.2 Comparison Operators
Comparison operators compare two operands and return True or False. Python allows chained comparisons
like 1 < x < 10 which is evaluated as 1 < x and x < 10.
Operator Meaning Example Result
== Equal to 'abc' == 'abc' True
!= Not equal to 5 != 3 True
> Greater than 7>3 True
< Less than 3<7 True
>= Greater than or 5 >= 5 True
equal
<= Less than or equal 3 <= 5 True
▸ Comparison Operators
# Chained comparisons (Pythonic!)
x = 15
print(10 < x < 20) # True (very readable!)
print(10 < x < 14) # False
print(1 < 2 < 3 < 4 < 5) # True
# Comparison with different types
print(1 == 1.0) # True (int and float compared by value)
print(1 == True) # True (True is 1, False is 0 in Python!)
print(0 == False) # True
# String comparison — lexicographic (character by character)
print('apple' < 'banana') # True ('a' < 'b')
print('abc' < 'abd') # True ('c' < 'd')
print('Z' < 'a') # True (uppercase < lowercase in ASCII)
12.3 Assignment Operators
Assignment operators assign or update variable values. Augmented assignment (+=, -=, etc.) is shorthand that
reads, operates, then writes back to the same variable. They work on any type that supports the operation.
▸ Assignment and Walrus Operators
x = 10
x += 5 # x = x + 5 → 15
x -= 3 # x = x - 3 → 12
x *= 2 # x = x * 2 → 24
x //= 5 # x = x // 5 → 4
x **= 3 # x = x ** 3 → 64
x %= 10 # x = x % 10 → 4
print(x) # 4
# Works with strings and lists too!
s = 'Hello'
s += ' World' # s = 'Hello World'
lst = [1, 2]
lst += [3, 4] # lst = [1, 2, 3, 4]
lst *= 2 # lst = [1,2,3,4,1,2,3,4]
# Walrus operator := (Python 3.8+)
# Assigns AND returns value in one expression
import random
while (n := [Link](1,6)) != 6:
print(f'Rolled {n}, try again')
print(f'Finally rolled {n}!')
12.4 Logical Operators
Short-circuit evaluation: Python evaluates logical expressions lazily. For and: stops if left is False. For or: stops
if left is True. This is not just efficient — it also enables safe patterns like x != 0 and (10/x > 1).
Operator Truth Rule Short-circuits When Returns
and True only if BOTH are True Left is falsy The first falsy value, or last value
or True if AT LEAST ONE is Left is truthy The first truthy value, or last value
True
not Inverts boolean Never True or False (always bool)
▸ Logical Operators — Truthy/Falsy
# Logical operators return VALUES not just True/False
print(0 and 'hello') # 0 (0 is falsy, short-circuits)
print(5 and 'hello') # hello (5 is truthy, returns right side)
print(0 or 'default') # default (0 is falsy, returns right side)
print(5 or 'default') # 5 (5 is truthy, short-circuits)
# Practical: provide default values
name = '' or 'Anonymous' # name = 'Anonymous'
value = None or 0 # value = 0
# Truthy and Falsy values in Python
# Falsy: False, 0, 0.0, '', [], {}, (), set(), None
# Truthy: everything else
print(bool('')) # False
print(bool([])) # False
print(bool(0)) # False
print(bool(None)) # False
print(bool('hi')) # True
print(bool([1])) # True
# not always returns bool
print(not '') # True
print(not 0) # True
print(not [1,2,3]) # False
12.5 Bitwise, Identity & Membership Operators
Category Operators Use Case
Bitwise & | ^ ~ << >> Binary data, flags, permissions, optimization
Identity is, is not Check if two vars point to same object in RAM
Membership in, not in Check if value exists in string/list/dict/set
▸ Identity, Membership, Bitwise
# IDENTITY: is vs ==
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True (same VALUES)
print(a is b) # False (different OBJECTS in memory)
print(a is c) # True (c points to same object as a)
print(id(a), id(b), id(c)) # a==c have same id
# Always use 'is' to check None, True, False
x = None
if x is None: # Correct (PEP 8)
print('x is None')
# MEMBERSHIP
vowels = 'aeiou'
print('a' in vowels) # True
print('z' not in vowels) # True
primes = {2, 3, 5, 7, 11}
print(7 in primes) # True — O(1) for sets!
# BITWISE example — checking/setting flags
READ = 0b100 # 4
WRITE = 0b010 # 2
EXEC = 0b001 # 1
perms = READ | WRITE # 0b110 = 6 (read + write)
print(perms & READ != 0) # True — has read permission
print(perms & EXEC != 0) # False — no execute permission
12.6 Operator Precedence
When multiple operators appear in an expression, Python evaluates them in a fixed order (precedence). Use
parentheses () to control evaluation order explicitly.
Level Operator(s) Associativity Example
1 (High) () Left → Right (2+3)*4 = 20
2 ** Right → Left 2**3**2 = 512 (not 64)
3 +x, -x, ~x Right → Left --5 = 5
4 *, /, //, % Left → Right 10/2*5 = 25
5 +, - Left → Right 10-3+2 = 9
6 <<, >> Left → Right 8>>1 = 4
7 & Left → Right Bitwise AND
8 ^ Left → Right Bitwise XOR
9 | Left → Right Bitwise OR
10 ==,!=,<,>,<=,>=,is,in Left → Right Comparisons
11 not Right → Left Logical NOT
12 and Left → Right Logical AND
13 (Low) or Left → Right Logical OR
PART 6
CONTROL FLOW — CONDITIONS & LOOPS
Chapter 13: Conditional Statements
Definition: Control flow determines the ORDER in which statements execute. By default, Python runs code top-
to-bottom. Conditionals let the program branch — choose different code paths based on conditions. Loops let
code repeat.
13.1 if / elif / else
The if statement evaluates a condition. If True, its block runs. elif ('else if') checks additional conditions. else
runs when all conditions are False. Python uses indentation (4 spaces) to define blocks, unlike other languages
that use braces.
▸ if / elif / else — Complete Guide
# Basic if
temperature = 35
if temperature > 30:
print('It is hot') # prints this
print('Wear light clothes')
# if-else
score = 45
if score >= 50:
print('Pass')
else:
print('Fail') # prints this
# if-elif-else (multiple branches)
marks = 78
if marks >= 90:
grade = 'A+'
elif marks >= 80:
grade = 'A'
elif marks >= 70:
grade = 'B'
elif marks >= 60:
grade = 'C'
elif marks >= 50:
grade = 'D'
else:
grade = 'F'
print(f'Grade: {grade}') # Grade: B
# Nested if
age = 25
has_id = True
if age >= 18:
if has_id:
print('Entry allowed')
else:
print('Need ID')
else:
print('Too young')
13.2 Ternary (Conditional Expression)
The ternary expression is a one-line if-else. Syntax: value_if_true if condition else
value_if_false. It is an expression (returns a value) not a statement.
▸ Ternary Operator
# Ternary operator
age = 20
label = 'Adult' if age >= 18 else 'Minor'
print(label) # Adult
# Inline in print
n = -5
print('Positive' if n > 0 else 'Zero' if n == 0 else 'Negative')
# Negative
# With function calls
nums = [3, 1, 4, 1, 5, 9]
result = max(nums) if nums else 0 # safe: handles empty list
# In list comprehension
data = [1, -2, 3, -4, 5]
abs_data = [x if x >= 0 else -x for x in data]
print(abs_data) # [1, 2, 3, 4, 5]
13.3 match-case (Python 3.10+)
Definition: Structural pattern matching allows matching values against patterns. It is more powerful than
switch-case in other languages — it can match values, types, sequences, mappings, and class instances.
▸ match-case — Structural Pattern Matching
# Simple value matching
day = 'Monday'
match day:
case 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday':
print('Weekday')
case 'Saturday' | 'Sunday':
print('Weekend')
case _:
print('Unknown')
# Matching with conditions (guard clauses)
point = (0, 5)
match point:
case (0, 0):
print('Origin')
case (x, 0):
print(f'On x-axis at {x}')
case (0, y):
print(f'On y-axis at {y}') # prints: On y-axis at 5
case (x, y) if x == y:
print(f'On diagonal at {x}')
case (x, y):
print(f'Point at ({x},{y})')
# Matching HTTP status codes
status = 404
match status:
case 200: msg = 'OK'
case 201: msg = 'Created'
case 400: msg = 'Bad Request'
case 404: msg = 'Not Found'
case 500: msg = 'Internal Server Error'
case _: msg = f'Unknown: {status}'
print(msg) # Not Found
Chapter 14: Loops
Definition (GFG): Loops allow a block of code to be executed repeatedly. Python provides two loop types: for
(iterate over a sequence/iterable) and while (repeat while condition is True). Loops are fundamental to
automation, data processing, and algorithms.
14.1 for Loop — Complete Guide
The for loop in Python is an iterator-based loop — it calls iter() on the object and repeatedly calls next()
until StopIteration. It works on ANY iterable.
▸ for Loop — All Forms
# Iterating over different iterables
# 1. List
for item in [10, 20, 30]:
print(item, end=' ') # 10 20 30
print()
# 2. String
for ch in 'Python':
print(ch, end='-') # P-y-t-h-o-n-
print()
# 3. range(stop), range(start,stop), range(start,stop,step)
for i in range(5): # 0 1 2 3 4
print(i, end=' ')
print()
for i in range(1, 10, 2): # 1 3 5 7 9
print(i, end=' ')
print()
for i in range(10, 0, -2): # 10 8 6 4 2 (countdown)
print(i, end=' ')
print()
# 4. Tuple unpacking in for
coords = [(1,2), (3,4), (5,6)]
for x, y in coords:
print(f'({x},{y})', end=' ') # (1,2) (3,4) (5,6)
print()
# 5. Dictionary
student = {'name': 'Alice', 'age': 20, 'gpa': 3.8}
for key in student: # iterates over keys
print(key, ':', student[key])
for k, v in [Link](): # key-value pairs
print(f'{k} = {v}')
# 6. enumerate() — get index AND value
fruits = ['apple', 'banana', 'cherry']
for i, fruit in enumerate(fruits, start=1):
print(f'{i}. {fruit}')
# 7. zip() — iterate multiple lists together
names = ['Alice', 'Bob', 'Carol']
scores = [95, 87, 92]
for name, score in zip(names, scores):
print(f'{name}: {score}')
14.2 while Loop
▸ while Loop
# Basic while
n = 1
while n <= 5:
print(n, end=' ') # 1 2 3 4 5
n += 1
print()
# while with else
i = 0
while i < 3:
print(i, end=' ') # 0 1 2
i += 1
else:
print('— done') # runs when condition becomes False
# do-while equivalent (Python has no do-while)
while True:
user_input = 'yes' # simulate: input('Continue? ')
if user_input.lower() != 'yes':
break
print('Running...')
break # remove this in real code
# Common pattern: reading until sentinel
data = [5, 3, 8, 1, 0, 9] # simulate: 0 = stop signal
idx = 0
while idx < len(data) and data[idx] != 0:
print(data[idx], end=' ') # 5 3 8 1
idx += 1
14.3 break, continue, pass, else on loops
▸ break, continue, pass, loop-else
# break — exit immediately
for i in range(1, 11):
if i == 5:
break
print(i, end=' ') # 1 2 3 4
print('—done')
# continue — skip to next iteration
for i in range(10):
if i % 3 == 0:
continue # skip multiples of 3
print(i, end=' ') # 1 2 4 5 7 8
print()
# pass — no-op placeholder
for i in range(5):
if i == 2:
pass # do nothing, continue normally
print(i, end=' ') # 0 1 2 3 4
print()
# else on for/while — runs ONLY if no break occurred
def find_prime(n):
for d in range(2, int(n**0.5)+1):
if n % d == 0:
print(f'{n} = {d} x {n//d}')
break
else:
print(f'{n} is prime!') # only if no break
find_prime(17) # 17 is prime!
find_prime(18) # 18 = 2 x 9
PART 7
FUNCTIONS — COMPLETE IN-DEPTH GUIDE
Chapter 15: Functions
Definition (GFG + W3Schools): A function is a named, reusable block of code that performs a specific task.
Functions follow the DRY principle (Don't Repeat Yourself). In Python, functions are first-class objects — they
can be stored in variables, passed as arguments, returned from other functions, and stored in data structures.
✅ Benefits of Functions
Reusability — Write once, call many times from anywhere
Modularity — Break complex programs into manageable pieces
Readability — Well-named functions make code self-documenting
Testability — Individual functions can be tested in isolation
Maintainability — Fix a bug once, it's fixed everywhere the function is called
15.1 Defining and Calling
▸ Defining Functions
# def keyword defines a function
# Syntax: def function_name(parameters): body
def greet():
print('Hello!')
greet() # Call: Hello!
# With parameters
def greet_person(name, greeting='Hello'):
print(f'{greeting}, {name}!')
greet_person('Alice') # Hello, Alice!
greet_person('Bob', 'Hi') # Hi, Bob!
# With return value
def square(n):
return n ** 2
result = square(5) # 25
# Multiple return values (returns a tuple)
def stats(numbers):
return min(numbers), max(numbers), sum(numbers)/len(numbers)
lo, hi, avg = stats([4, 2, 8, 6, 1])
print(lo, hi, avg) # 1 8 4.2
# Functions are objects
my_func = greet # assign to variable
my_func() # Hello!
funcs = [square, abs, bool] # store in list
for f in funcs:
print(f(-5)) # 25, 5, True
15.2 Argument Types — Complete Guide
Python supports 5 types of function arguments. The order in a function signature MUST be: positional →
*args → keyword-only → **kwargs.
▸ Function Arguments — All 5 Types
# 1. Positional Arguments — matched by position
def full_name(first, last):
return f'{first} {last}'
print(full_name('Alice', 'Smith')) # Alice Smith
# 2. Default Arguments — used when not provided
def power(base, exp=2):
return base ** exp
print(power(3)) # 9 (uses default exp=2)
print(power(3, 3)) # 27 (overrides default)
# 3. Keyword Arguments — passed by name
def create_user(name, age, city):
print(f'{name}, {age}, {city}')
create_user(age=25, city='NYC', name='Bob')
# 4. *args — variable positional (tuple)
def total(*numbers):
print(type(numbers)) # <class 'tuple'>
return sum(numbers)
print(total(1, 2, 3, 4, 5)) # 15
# 5. **kwargs — variable keyword (dict)
def profile(**info):
print(type(info)) # <class 'dict'>
for k, v in [Link]():
print(f' {k}: {v}')
profile(name='Alice', age=30, job='Dev')
# Combined: all 5 types
def mixed(a, b=10, *args, key='default', **kwargs):
print(f'a={a}, b={b}, args={args}, key={key}, kwargs={kwargs}')
mixed(1, 2, 3, 4, key='X', x=100)
# a=1, b=2, args=(3,4), key=X, kwargs={'x':100}
# Unpacking arguments with * and **
def add(x, y, z):
return x + y + z
nums = [1, 2, 3]
print(add(*nums)) # Unpack list → positional
d = {'x': 10, 'y': 20, 'z': 30}
print(add(**d)) # Unpack dict → keyword
15.3 Variable Scope — LEGB Rule
LEGB Rule: Python resolves variable names in this order: Local → Enclosing → Global → Built-in. This is the
scope chain.
▸ LEGB Scope Rule
# L — Local: inside current function
def func():
x = 'local' # local variable
print(x) # local
# G — Global: module level
x = 'global'
def func2():
print(x) # reads global x → 'global'
# global keyword — modify global from inside function
count = 0
def increment():
global count # declare intent to modify global
count += 1
increment()
increment()
print(count) # 2
# E — Enclosing: outer function (closures)
def outer():
msg = 'hello' # enclosing variable
def inner():
print(msg) # reads from enclosing scope
inner() # hello
# nonlocal keyword — modify enclosing variable
def make_counter():
count = 0
def counter():
nonlocal count # modify enclosing count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
# B — Built-in: Python's built-in names
print(len([1,2,3])) # len is in builtins
print(type(42)) # type is in builtins
15.4 Lambda Functions
Definition: Lambda functions are anonymous single-expression functions. They cannot contain statements like
if blocks or for loops (only expressions). Best used for short, throwaway functions passed to
map/filter/sorted.
▸ Lambda Functions
# Syntax: lambda parameters: expression
# Basic
square = lambda x: x ** 2
print(square(7)) # 49
# Multiple parameters
add = lambda a, b: a + b
print(add(3, 5)) # 8
# Lambda with default value
greet = lambda name, msg='Hi': f'{msg}, {name}!'
print(greet('Alice')) # Hi, Alice!
print(greet('Bob', 'Hello')) # Hello, Bob!
# With sorted() — custom sort key
students = [('Alice', 85), ('Bob', 92), ('Charlie', 78)]
by_score = sorted(students, key=lambda s: s[1])
print(by_score) # Charlie, Alice, Bob
by_name = sorted(students, key=lambda s: s[0])
print(by_name) # Alice, Bob, Charlie
# With map() — apply to each element
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
print(doubled) # [2, 4, 6, 8, 10]
# With filter() — keep elements where True
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4]
# Immediately Invoked Lambda
result = (lambda x, y: x ** y)(2, 10)
print(result) # 1024
15.5 Recursion
Definition: Recursion is when a function calls itself. Every recursive function needs: (1) Base case — the
condition that STOPS recursion; (2) Recursive case — smaller sub-problem that APPROACHES the base case.
Python's default recursion limit is 1000 ([Link]() changes it).
▸ Recursion
# Factorial: n! = n * (n-1) * ... * 1
def factorial(n):
if n <= 1: # BASE CASE
return 1
return n * factorial(n-1) # RECURSIVE CASE
# Trace: factorial(4)
# factorial(4) = 4 * factorial(3)
# = 4 * 3 * factorial(2)
# = 4 * 3 * 2 * factorial(1)
# = 4 * 3 * 2 * 1 = 24
print(factorial(5)) # 120
# Fibonacci
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2)
print([fib(i) for i in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Faster: Memoized Fibonacci
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_fast(n):
if n <= 1: return n
return fib_fast(n-1) + fib_fast(n-2)
print(fib_fast(50)) # 12586269025 (instant!)
# Recursive sum of list
def rsum(lst):
if not lst: return 0 # base: empty list
return lst[0] + rsum(lst[1:]) # head + rest
print(rsum([1,2,3,4,5])) # 15
15.6 Decorators
Definition (GFG): A decorator is a function that takes another function as input and returns a modified version
of it. Decorators use the @ syntax (syntactic sugar). They are used for: logging, timing, authentication, caching,
validation, and more. This is the wrapper pattern.
▸ Decorators — Complete Guide
# Step 1: Understand — function that takes a function
def my_decorator(func):
def wrapper(*args, **kwargs):
print('Before the function runs')
result = func(*args, **kwargs) # call original
print('After the function runs')
return result
return wrapper
# Step 2: Apply with @
@my_decorator
def say_hello(name):
print(f'Hello, {name}!')
return f'Hello, {name}!'
say_hello('Alice')
# Before the function runs
# Hello, Alice!
# After the function runs
# Practical: Timer decorator
import time
def timer(func):
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f'{func.__name__} took {end-start:.4f}s')
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
print(slow_sum(1000000)) # slow_sum took 0.0XXs
# Decorator with arguments
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f'Hi {name}!')
greet('Bob') # prints 3 times
PART 8
EXCEPTION HANDLING — COMPLETE GUIDE
Chapter 16: Exceptions
Definition (GFG): An exception is an error that occurs during program execution (runtime). When Python
encounters an error it cannot handle, it raises an exception object. If uncaught, it prints a traceback and
terminates the program. Exception handling lets us catch errors and respond gracefully instead of crashing.
16.1 Exception Hierarchy
All Python exceptions inherit from BaseException. Most user-level exceptions inherit from Exception.
▸ Exception Hierarchy
# Python Exception Hierarchy (simplified)
# BaseException
# ├── SystemExit
# ├── KeyboardInterrupt
# ├── GeneratorExit
# └── Exception
# ├── ArithmeticError
# │ ├── ZeroDivisionError
# │ ├── OverflowError
# │ └── FloatingPointError
# ├── LookupError
# │ ├── IndexError
# │ └── KeyError
# ├── ValueError
# ├── TypeError
# ├── NameError
# │ └── UnboundLocalError
# ├── AttributeError
# ├── ImportError
# │ └── ModuleNotFoundError
# ├── OSError
# │ ├── FileNotFoundError
# │ ├── PermissionError
# │ └── TimeoutError
# ├── RuntimeError
# │ └── RecursionError
# └── StopIteration
16.2 Common Exceptions
Exception When It Occurs Example That Raises It
ZeroDivisionError Division or modulo by zero 10 / 0
ValueError Right type, wrong value int('abc') or int('')
TypeError Wrong type for operation 'hi' + 5 or len(42)
IndexError Sequence index out of range [1,2][10]
KeyError Dict key doesn't exist {'a':1}['b']
AttributeError Object has no such attribute 'hello'.unknown_method()
NameError Variable/function not defined print(undefined_var)
FileNotFoundError File path doesn't exist open('[Link]')
PermissionError No permission to access file open('/etc/shadow')
ImportError / Module not found import fake_module
ModuleNotFoundError
RecursionError Recursion depth exceeded def f(): f() — no base case
OverflowError Result too large for float import math; [Link](1000)
StopIteration Iterator is exhausted next(iter([]))
UnicodeDecodeError Can't decode bytes to string b'\xff'.decode('utf-8')
MemoryError Not enough memory [0] * 10**12
16.3 try / except / else / finally
The full exception handling structure has 4 blocks: try (risky code), except (handle error), else (runs if NO
exception), finally (ALWAYS runs, even if exception or return).
▸ try/except/else/finally
# Full structure
try:
# Code that might raise an exception
num = int(input('Number: ')) # might raise ValueError
result = 100 / num # might raise ZeroDivisionError
except ValueError:
print('Invalid number!')
except ZeroDivisionError:
print('Cannot divide by zero!')
except (TypeError, AttributeError) as e:
print(f'Type/Attr error: {e}') # catch multiple
except Exception as e:
# Catches ANY remaining exception
print(f'Unexpected error: {type(e).__name__}: {e}')
else:
# Runs ONLY if no exception occurred in try
print(f'Success! Result = {result}')
finally:
# ALWAYS runs (cleanup code here)
print('Done — always printed')
# Practical: file handling with finally
f = None
try:
f = open('[Link]', 'r')
content = [Link]()
except FileNotFoundError:
print('File not found')
finally:
if f: # ensure file is closed
[Link]()
print('File closed')
16.4 Raising and Custom Exceptions
▸ Custom Exceptions
# raise — manually trigger an exception
def validate_age(age):
if not isinstance(age, int):
raise TypeError(f'Age must be int, got {type(age).__name__}')
if age < 0:
raise ValueError('Age cannot be negative')
if age > 150:
raise ValueError('Age is unrealistically large')
return age
try:
validate_age(-5)
except ValueError as e:
print(f'ValueError: {e}') # ValueError: Age cannot be negative
# Custom Exception Classes
class AppError(Exception):
'''Base for all app errors'''
pass
class InsufficientFundsError(AppError):
def __init__(self, balance, amount):
[Link] = balance
[Link] = amount
[Link] = amount - balance
super().__init__(
f'Need {amount}, have {balance} (short by {[Link]})'
)
class AccountLockedError(AppError):
pass
def withdraw(account, amount):
if [Link]('locked'):
raise AccountLockedError('Account is locked')
if amount > account['balance']:
raise InsufficientFundsError(account['balance'], amount)
account['balance'] -= amount
return account['balance']
acct = {'balance': 100, 'locked': False}
try:
withdraw(acct, 500)
except InsufficientFundsError as e:
print(e) # Need 500, have 100 (short by 400)
print([Link]) # 400
PART 9
DATA STRUCTURES — LISTS, TUPLES, SETS, DICTS
Chapter 17: Lists — Complete Guide
Definition (GFG): A list is an ordered, mutable sequence that stores any mix of data types. Lists are Python's
most versatile container. They are implemented as dynamic arrays — when full, Python allocates more space.
Indexing is O(1), append is O(1) amortized, but insert/delete at middle is O(n).
17.1 Creating and Accessing
▸ List Creation and Access
# Creating lists
empty = []
nums = [1, 2, 3, 4, 5]
mixed = [42, 'hello', 3.14, True, None, [1,2]]
from_range = list(range(0, 20, 3)) # [0, 3, 6, 9, 12, 15, 18]
from_string = list('Python') # ['P','y','t','h','o','n']
repeated = [0] * 5 # [0, 0, 0, 0, 0]
# Indexing
lst = ['a', 'b', 'c', 'd', 'e']
print(lst[0]) # 'a' — first
print(lst[-1]) # 'e' — last
print(lst[-2]) # 'd' — second from end
# Slicing: lst[start:stop:step] (stop is EXCLUDED)
print(lst[1:4]) # ['b','c','d']
print(lst[:3]) # ['a','b','c']
print(lst[2:]) # ['c','d','e']
print(lst[::2]) # ['a','c','e'] — every other
print(lst[::-1]) # ['e','d','c','b','a'] — reversed
# Modify elements
lst[0] = 'A'
lst[1:3] = ['B', 'C'] # slice assignment
print(lst) # ['A', 'B', 'C', 'd', 'e']
17.2 All List Methods
Method Syntax Description Time Complexity
append [Link](x) Add x at end O(1)
insert [Link](i, x) Insert x before index i O(n)
extend [Link](iterable) Append all items from iterable O(k)
remove [Link](x) Remove first occurrence of x O(n)
pop [Link](i=-1) Remove & return item at i O(1) end, O(n) middle
clear [Link]() Remove all items O(n)
index [Link](x,start,end) Index of first x O(n)
count [Link](x) Count occurrences of x O(n)
sort [Link](key, reverse) Sort in-place O(n log n)
reverse [Link]() Reverse in-place O(n)
copy [Link]() Shallow copy O(n)
sorted() sorted(lst, key, rev) Returns NEW sorted list O(n log n)
len() len(lst) Number of items O(1)
min/max min(lst), max(lst) Smallest/largest O(n)
sum() sum(lst) Sum of items O(n)
▸ List Methods in Depth
# Comprehensive list operations
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
[Link](7) # [3,1,4,1,5,9,2,6,5,3,7]
[Link](0, 0) # [0,3,1,4,...]
[Link]([8, 9]) # adds 8,9 at end
[Link](1) # removes first 1
popped = [Link]() # removes and returns last
popped2 = [Link](0) # removes index 0
idx = [Link](5) # finds index of first 5
cnt = [Link](3) # how many 3s
# Sorting
words = ['banana', 'apple', 'cherry', 'date']
[Link]() # alphabetical in-place
[Link](reverse=True) # reverse alphabetical
[Link](key=len) # sort by length
[Link](key=lambda w: w[-1]) # sort by last char
# sorted() — doesn't modify original
original = [3, 1, 4, 1, 5]
new = sorted(original, reverse=True) # [5,4,3,1,1]
print(original) # [3, 1, 4, 1, 5] — unchanged!
# List concatenation and multiplication
a = [1, 2, 3]
b = [4, 5, 6]
print(a + b) # [1,2,3,4,5,6]
print(a * 3) # [1,2,3,1,2,3,1,2,3]
17.3 List Comprehensions
Definition: List comprehension is a concise way to create lists using a single expression. It is more readable and
faster than equivalent for loops.
▸ List Comprehensions
# Basic: [expression for item in iterable]
squares = [x**2 for x in range(1, 11)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# With condition: [expr for item in iter if condition]
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# With if-else
labels = ['even' if x%2==0 else 'odd' for x in range(1,6)]
# ['odd', 'even', 'odd', 'even', 'odd']
# Nested comprehension (matrix)
matrix = [[i*j for j in range(1,4)] for i in range(1,4)]
# [[1,2,3],[2,4,6],[3,6,9]]
# Flatten nested list
nested = [[1,2,3],[4,5],[6,7,8,9]]
flat = [x for row in nested for x in row]
# [1,2,3,4,5,6,7,8,9]
# String processing
words = ['hello', 'WORLD', 'Python']
lower = [[Link]() for w in words]
upper_long = [[Link]() for w in words if len(w) > 4]
# Filter and transform
data = [3, -1, 4, -1, 5, -9, 2, -6]
positives = [x for x in data if x > 0] # [3,4,5,2]
abs_vals = [abs(x) for x in data] # [3,1,4,1,5,9,2,6]
17.4 Copying Lists — Shallow vs Deep
▸ Shallow vs Deep Copy
# Shallow copy — copies the list, NOT nested objects
original = [1, 2, [3, 4], 5]
shallow1 = [Link]()
shallow2 = original[:]
shallow3 = list(original)
# Changing top-level is safe
shallow1[0] = 99
print(original[0]) # 1 — unchanged
# But nested objects are SHARED
shallow1[2][0] = 999
print(original[2]) # [999, 4] — CHANGED! (shared reference)
# Deep copy — independent copy of everything
import copy
deep = [Link](original)
deep[2][0] = 777
print(original[2]) # [999, 4] — unchanged now
Chapter 18: Tuples
Definition (GFG): A tuple is an ordered, immutable sequence. Once created, elements cannot be changed.
Tuples are stored more efficiently than lists, can be used as dictionary keys (since they're hashable), and are
often used to return multiple values from functions.
▸ Tuples — Complete
# Creating tuples
empty = ()
single = (42,) # MUST have trailing comma for single item
wrong = (42) # This is just parentheses around 42 — NOT a tuple!
print(type(single)) # <class 'tuple'>
print(type(wrong)) # <class 'int'>
t = (10, 20, 30, 40, 50)
print(t[0]) # 10
print(t[-1]) # 50
print(t[1:4]) # (20, 30, 40)
# Packing and Unpacking
point = 3, 4 # packing (parens optional)
x, y = point # unpacking
print(x, y) # 3 4
# Extended unpacking with *
first, *rest = (1, 2, 3, 4, 5)
print(first) # 1
print(rest) # [2, 3, 4, 5]
*init, last = (1, 2, 3, 4, 5)
print(init) # [1, 2, 3, 4]
print(last) # 5
a, *mid, b = (1, 2, 3, 4, 5)
print(a, mid, b) # 1 [2,3,4] 5
# Tuple as dict key
locations = {(0,0): 'origin', (1,0): 'right', (0,1): 'up'}
print(locations[(0,0)]) # origin
# Named tuple for structured data
from collections import namedtuple
Person = namedtuple('Person', ['name', 'age', 'city'])
alice = Person('Alice', 30, 'NYC')
print([Link]) # Alice
print(alice[1]) # 30 (index access still works)
print(alice._asdict()) # {'name': 'Alice', 'age': 30, 'city': 'NYC'}
Chapter 19: Sets
Definition (GFG): A set is an unordered, mutable collection of unique hashable elements. Sets are
implemented as hash tables, making membership testing O(1). They are ideal for removing duplicates and
performing set algebra (union, intersection, difference).
▸ Sets — Complete
# Creating sets
s = {1, 2, 3, 4, 5}
from_list = set([1, 2, 2, 3, 3, 3]) # {1, 2, 3} — dups removed!
from_str = set('hello') # {'h','e','l','o'} — unique chars
empty_set = set() # NOT {} — that creates empty dict!
# Modifying sets
[Link](6) # add single element
[Link]([7,8,9]) # add multiple elements
[Link](1) # remove — raises KeyError if not found
[Link](99) # remove — NO error if not found
popped = [Link]() # remove and return arbitrary element
# Set operations
A = {1, 2, 3, 4, 5}
B = {4, 5, 6, 7, 8}
print(A | B) # Union: {1,2,3,4,5,6,7,8}
print(A & B) # Intersection: {4,5}
print(A - B) # Difference (in A not B): {1,2,3}
print(B - A) # Difference (in B not A): {6,7,8}
print(A ^ B) # Symmetric diff: {1,2,3,6,7,8}
# Equivalents using methods
print([Link](B))
print([Link](B))
print([Link](B))
print(A.symmetric_difference(B))
# Subset, superset, disjoint
print({1,2}.issubset({1,2,3,4})) # True
print({1,2,3,4}.issuperset({1,2})) # True
print({1,2}.isdisjoint({3,4})) # True (no common elements)
# frozenset — immutable set (can be dict key)
fs = frozenset([1, 2, 3])
d = {fs: 'value'} # works as dict key!
Chapter 20: Dictionaries
Definition (GFG/W3Schools): A dictionary is an ordered (Python 3.7+), mutable collection of key-value pairs.
Keys must be unique and immutable (string, int, tuple). Dictionaries are implemented as hash tables with O(1)
average lookup, insertion, and deletion.
▸ Dictionaries — Complete
# Creating dictionaries
empty = {}
person = {'name': 'Alice', 'age': 25, 'city': 'NYC'}
# Using dict() constructor
d1 = dict(name='Bob', age=30)
d2 = dict([('a', 1), ('b', 2)]) # from list of tuples
d3 = [Link](['x','y','z'], 0) # {'x':0,'y':0,'z':0}
# Accessing
print(person['name']) # Alice
print([Link]('age')) # 25
print([Link]('job', 'N/A')) # N/A (default)
# Adding / Updating
person['email'] = 'alice@[Link]' # add new key
person['age'] = 26 # update existing
[Link]({'age': 27, 'job': 'Dev'}) # update multiple
# Deleting
del person['email']
job = [Link]('job') # removes & returns
[Link]() # removes last inserted (3.7+)
# Iterating
for key in person: # iterate keys
print(key)
for key in [Link](): # explicit keys
print(key)
for val in [Link](): # values
print(val)
for k, v in [Link](): # key-value pairs
print(f'{k}: {v}')
# Nested dict
students = {
'Alice': {'age': 20, 'grade': 'A'},
'Bob': {'age': 22, 'grade': 'B'},
}
print(students['Alice']['grade']) # A
# Dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
# {1:1, 2:4, 3:9, 4:16, 5:25}
# Merging dicts (Python 3.9+)
d1 = {'a': 1, 'b': 2}
d2 = {'b': 3, 'c': 4}
merged = d1 | d2 # {'a':1, 'b':3, 'c':4} (d2 wins on conflict)
d1 |= d2 # update d1 in-place
Method What It Returns / Does
keys() View of all keys (updates dynamically)
values() View of all values
items() View of (key, value) pairs
get(k, default=None) Value for k, or default (no KeyError)
pop(k, default) Remove k and return its value
popitem() Remove and return last inserted (k,v) pair
update(d2) Merge d2 into dict (d2 values overwrite)
setdefault(k, v) Return value if k exists, else set k=v and return v
clear() Remove all items
copy() Shallow copy
fromkeys(keys, v) Class method: new dict with given keys, all set to v
PART 10
OBJECT-ORIENTED PROGRAMMING — COMPLETE DEEP DIVE
Chapter 21: OOP Fundamentals
Definition (GFG): Object-Oriented Programming (OOP) is a programming paradigm that organizes code around
objects — entities that bundle related data (attributes) and behavior (methods) together. Everything in Python
is an object — every integer, string, list, function, and module is an instance of some class.
The Four Pillars of OOP
1. Encapsulation — Bundle data + methods; hide internal details from outside
2. Abstraction — Show only what's necessary; hide complex implementation
3. Inheritance — Child class inherits attributes and methods from parent class
4. Polymorphism — Same method name behaves differently in different classes
21.1 Classes and Objects
Class: A blueprint or template that defines attributes and methods. Object: An instance created from a class —
an actual realization of the blueprint. self: A reference to the current instance. It must be the first parameter of
every instance method.
▸ Complete Class Example — BankAccount
class BankAccount:
# Class variable: shared by ALL instances
bank_name = 'Python National Bank'
total_accounts = 0 # track count
# Constructor: called when object is created
def __init__(self, owner, balance=0):
# Instance variables: unique to each object
[Link] = owner
[Link] = balance
self._transactions = [] # 'protected' by convention
BankAccount.total_accounts += 1
# Instance method
def deposit(self, amount):
if amount <= 0:
raise ValueError('Amount must be positive')
[Link] += amount
self._transactions.append(('deposit', amount))
print(f'Deposited {amount}. Balance: {[Link]}')
def withdraw(self, amount):
if amount > [Link]:
raise ValueError(f'Insufficient funds: {[Link]}')
[Link] -= amount
self._transactions.append(('withdraw', amount))
return amount
def get_statement(self):
print(f'--- Statement for {[Link]} ---')
for txn_type, amt in self._transactions:
print(f' {txn_type:10s}: {amt:>8.2f}')
print(f' Current balance: {[Link]:>8.2f}')
# Class method — works with class, not instance
@classmethod
def get_total_accounts(cls):
return cls.total_accounts
# Static method — no access to class or instance
@staticmethod
def is_valid_amount(amount):
return isinstance(amount, (int, float)) and amount > 0
def __str__(self):
return f'BankAccount({[Link]}, balance={[Link]})'
def __repr__(self):
return f'BankAccount(owner={[Link]!r}, balance={[Link]})'
# Using the class
acc1 = BankAccount('Alice', 1000)
acc2 = BankAccount('Bob')
[Link](500)
[Link](200)
acc1.get_statement()
print(BankAccount.bank_name) # Python National Bank
print(BankAccount.get_total_accounts()) # 2
print(BankAccount.is_valid_amount(100)) # True
print(acc1) # BankAccount(Alice, balance=1300)
21.2 Instance vs Class vs Static Methods
Method Type Decorator First Parameter Access To Use Case
Instance Method (none) self instance attrs + class attrs Most methods: operate on object
data
Class Method @classmethod cls class attrs only (not Factory methods, counters
instance)
Static Method @staticmethod (none) nothing (standalone) Utility functions related to class
▸ Instance vs Class vs Static Methods
class Temperature:
unit = 'Celsius' # class variable
def __init__(self, value):
[Link] = value # instance variable
# Instance method — needs self
def display(self):
return f'{[Link]}° {[Link]}'
# Class method — creates object from different format
@classmethod
def from_fahrenheit(cls, f):
celsius = (f - 32) * 5/9
return cls(celsius) # creates new Temperature instance
@classmethod
def set_unit(cls, unit):
[Link] = unit
# Static method — doesn't need class or instance
@staticmethod
def celsius_to_fahrenheit(c):
return c * 9/5 + 32
t1 = Temperature(100)
t2 = Temperature.from_fahrenheit(212) # creates from °F
print([Link]()) # 100° Celsius
print([Link]) # 100.0
print(Temperature.celsius_to_fahrenheit(0)) # 32.0
21.3 Encapsulation — Access Control
Definition: Encapsulation bundles data and methods together and restricts direct access to internal state.
Python uses naming conventions (not enforced by the language): public, _protected (single underscore),
and __private (double underscore — triggers name mangling).
▸ Encapsulation and Properties
class Student:
def __init__(self, name, age, gpa):
[Link] = name # PUBLIC: accessible anywhere
self._age = age # PROTECTED: 'please don't touch'
self.__gpa = gpa # PRIVATE: name-mangled to _Student__gpa
# Getter and Setter using @property
@property
def gpa(self):
'''Read-only access to gpa'''
return self.__gpa
@[Link]
def gpa(self, value):
if not 0.0 <= value <= 4.0:
raise ValueError('GPA must be between 0.0 and 4.0')
self.__gpa = value
@property
def age(self):
return self._age
@[Link]
def age(self, value):
if value < 0 or value > 120:
raise ValueError('Invalid age')
self._age = value
s = Student('Alice', 20, 3.8)
print([Link]) # Alice — OK (public)
print([Link]) # 3.8 — OK (via @property)
[Link] = 3.9 # OK (via @setter with validation)
# s.__gpa # AttributeError!
print(s._Student__gpa) # 3.9 — works but bad practice
# @property makes attributes look like attributes but act like methods
class Circle:
def __init__(self, radius):
[Link] = radius
@property
def area(self): # computed property
import math
return [Link] * [Link] ** 2
@property
def diameter(self):
return [Link] * 2
c = Circle(5)
print([Link]) # 78.539... (no parentheses!)
print([Link]) # 10
21.4 Inheritance — Complete Guide
Definition: Inheritance allows a class (subclass/child) to inherit all attributes and methods from another class
(superclass/parent). Python supports single, multiple, multilevel, hierarchical, and hybrid inheritance.
▸ Single Inheritance
# Single Inheritance
class Animal:
def __init__(self, name, sound):
[Link] = name
[Link] = sound
def speak(self):
return f'{[Link]} says {[Link]}'
def eat(self):
return f'{[Link]} is eating'
def __str__(self):
return f'{type(self).__name__}({[Link]})'
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, 'Woof') # call parent __init__
[Link] = breed
def fetch(self, item):
return f'{[Link]} fetches the {item}!'
def speak(self): # Override parent method
return f'{[Link]} barks: WOOF WOOF!'
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name, 'Meow')
[Link] = indoor
def purr(self):
return f'{[Link]} purrrrs...'
d = Dog('Rex', 'Labrador')
c = Cat('Whiskers')
print([Link]()) # Rex barks: WOOF WOOF!
print([Link]()) # Whiskers says Meow
print([Link]()) # Rex is eating (inherited)
print([Link]('ball')) # Rex fetches the ball!
print(d) # Dog(Rex)
# isinstance and issubclass
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True (is also Animal!)
print(issubclass(Dog, Animal)) # True
▸ Multiple and Multilevel Inheritance
# Multiple Inheritance
class Flyable:
def fly(self):
return f'{[Link]} is flying'
class Swimmable:
def swim(self):
return f'{[Link]} is swimming'
class Duck(Animal, Flyable, Swimmable):
def __init__(self, name):
super().__init__(name, 'Quack')
donald = Duck('Donald')
print([Link]()) # Donald says Quack
print([Link]()) # Donald is flying
print([Link]()) # Donald is swimming
# MRO — Method Resolution Order
# Python uses C3 linearization to determine order
print(Duck.__mro__)
# (<class 'Duck'>, <class 'Animal'>, <class 'Flyable'>,
# <class 'Swimmable'>, <class 'object'>)
# Multilevel Inheritance
class GuideDog(Dog):
def guide(self):
return f'{[Link]} guides its owner safely'
g = GuideDog('Buddy', 'Golden Retriever')
print([Link]()) # Buddy barks: WOOF WOOF! (from Dog)
print([Link]()) # Buddy is eating (from Animal)
print([Link]()) # Buddy guides its owner safely
print(GuideDog.__mro__)
# GuideDog → Dog → Animal → object
21.5 Polymorphism
Definition: Polymorphism means 'many forms'. The same method name can behave differently across classes.
Python uses duck typing — if an object has the right method, it can be used regardless of its actual type.
▸ Polymorphism and Duck Typing
# Method Overriding (Runtime Polymorphism)
class Shape:
def area(self):
raise NotImplementedError('Subclasses must implement area()')
def describe(self):
return f'I am a {type(self).__name__} with area {[Link]():.2f}'
class Circle(Shape):
def __init__(self, radius):
[Link] = radius
def area(self):
import math
return [Link] * [Link] ** 2
def perimeter(self):
import math
return 2 * [Link] * [Link]
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
def perimeter(self):
return 2 * (self.w + self.h)
class Triangle(Shape):
def __init__(self, base, height):
[Link], [Link] = base, height
def area(self):
return 0.5 * [Link] * [Link]
# Polymorphic usage — same interface, different behavior
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
for shape in shapes:
print([Link]()) # each calls its OWN area()
# Duck typing — no inheritance needed
class Duck:
def quack(self): return 'Quack!'
class Person:
def quack(self): return 'I\'m quacking like a duck!'
def make_it_quack(obj): # doesn't care about type
print([Link]())
make_it_quack(Duck()) # Quack!
make_it_quack(Person()) # I'm quacking like a duck!
21.6 Abstraction — Abstract Base Classes
▸ Abstract Base Classes
# Abstract classes define an interface that subclasses MUST implement
from abc import ABC, abstractmethod
class Vehicle(ABC): # ABC = Abstract Base Class
def __init__(self, make, model, year):
[Link] = make
[Link] = model
[Link] = year
@abstractmethod
def start(self):
pass # MUST be implemented by subclasses
@abstractmethod
def stop(self):
pass
# Concrete method — shared implementation
def info(self):
return f'{[Link]} {[Link]} {[Link]}'
class Car(Vehicle):
def start(self): # Must implement
return f'{[Link]()} engine starts: Vroom!'
def stop(self): # Must implement
return f'{[Link]()} brakes applied'
class ElectricCar(Vehicle):
def __init__(self, make, model, year, battery):
super().__init__(make, model, year)
[Link] = battery
def start(self):
return f'{[Link]()} starts silently (battery: {[Link]}kWh)'
def stop(self):
return f'{[Link]()} regenerative braking'
# v = Vehicle('a','b',2020) # TypeError — cannot instantiate abstract class
car = Car('Toyota', 'Camry', 2023)
ev = ElectricCar('Tesla', 'Model 3', 2024, 75)
print([Link]()) # 2023 Toyota Camry engine starts: Vroom!
print([Link]()) # 2024 Tesla Model 3 starts silently (battery: 75kWh)
21.7 Important Dunder (Magic) Methods
Definition: Dunder methods (double underscore) define how objects behave with Python's built-in operations.
They are automatically called by Python in specific situations — not usually called directly.
Method Triggered By Example Purpose
__init__(self,...) ClassName(...) Initialize object attributes
__str__(self) str(obj), print(obj) Human-readable string
__repr__(self) repr(obj), in console Developer/debug representation
__len__(self) len(obj) Define length
__getitem__(self,k) obj[k] Index/key access
__setitem__(self,k,v) obj[k] = v Set item
__delitem__(self,k) del obj[k] Delete item
__contains__(self,x) x in obj Membership test
__iter__(self) for x in obj Make iterable
__next__(self) next(obj) Iterator protocol
__add__(self,other) obj + other Addition
__sub__(self,other) obj - other Subtraction
__mul__(self,other) obj * other Multiplication
__eq__(self,other) obj == other Equality comparison
__lt__(self,other) obj < other Less-than comparison
__hash__(self) hash(obj) Make hashable (dict key)
__bool__(self) bool(obj), if obj: Truth value
__call__(self,...) obj(...) Make callable
__enter__/__exit__ with obj as x: Context manager
▸ Dunder Methods — Complete Vector Example
class Vector:
'''2D vector with full operator support'''
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self): # print(v) → Vector(3, 4)
return f'Vector({self.x}, {self.y})'
def __repr__(self): # repr(v)
return f'Vector({self.x!r}, {self.y!r})'
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other): # v1 - v2
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar): # v * 3
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # 3 * v
return self.__mul__(scalar)
def __eq__(self, other): # v1 == v2
return self.x == other.x and self.y == other.y
def __len__(self): # len(v)
return 2
def __abs__(self): # abs(v) — magnitude
return (self.x**2 + self.y**2) ** 0.5
def __bool__(self): # bool(v) — True if non-zero
return self.x != 0 or self.y != 0
def __neg__(self): # -v
return Vector(-self.x, -self.y)
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
print(3 * v1) # Vector(9, 12)
print(abs(v1)) # 5.0 (magnitude)
print(v1 == Vector(3,4)) # True
PART 11
PYTHON STANDARD LIBRARY — IMPORTANT MODULES
Chapter 22: os and sys Modules
22.1 os Module
Definition: The os module provides a portable way to interact with the operating system — file system
operations, environment variables, processes, and paths. It abstracts OS differences between Windows, Linux,
and macOS.
▸ os Module — Complete Reference
import os
# Current working directory
print([Link]()) # /home/user/projects
[Link]('/tmp') # change directory
# Directory operations
[Link]('new_folder') # create directory
[Link]('a/b/c') # create nested dirs
[Link]('new_folder') # remove empty dir
[Link]('a/b/c') # remove nested empty dirs
# File operations
[Link]('[Link]', '[Link]') # rename
[Link]('[Link]') # delete file
# Listing files
files = [Link]('.') # list current dir
print(files) # ['[Link]', 'folder', ...]
# Walking directory tree
for root, dirs, files in [Link]('.'):
print(f'Dir: {root}')
for f in files:
print(f' File: {f}')
# File info
print([Link]('[Link]')) # True/False
print([Link]('[Link]')) # True if file
print([Link]('folder')) # True if directory
print([Link]('[Link]')) # size in bytes
# Path manipulation
print([Link]('folder', 'sub', '[Link]')) # folder/sub/[Link]
print([Link]('/path/to/[Link]')) # [Link]
print([Link]('/path/to/[Link]')) # /path/to
print([Link]('[Link]')) # ('file', '.txt')
print([Link]('[Link]')) # full absolute path
# Environment variables
home = [Link]('HOME', '/default')
path = [Link]['PATH']
[Link]['MY_VAR'] = 'hello'
# Run system command
[Link]('echo Hello World')
result = [Link]('ls -la').read() # capture output
22.2 sys Module
Definition: The sys module provides access to Python interpreter variables and functions — command-line
arguments, stdin/stdout/stderr, the module search path, and interpreter information.
▸ sys Module
import sys
# Python version info
print([Link]) # '3.11.0 (main, ...)'
print(sys.version_info) # sys.version_info(major=3, minor=11, ...)
print(sys.version_info.major) # 3
# Command-line arguments
# Run: python [Link] arg1 arg2
print([Link]) # ['[Link]', 'arg1', 'arg2']
print([Link][0]) # '[Link]' (script name)
print([Link][1:]) # ['arg1', 'arg2']
# Module search paths
print([Link]) # list of dirs Python searches for modules
[Link](0, '/my/custom/modules') # add custom path
# Standard input/output/error
[Link]('Hello\n') # same as print()
[Link]('Error!\n') # write to stderr
# Exit the program
# [Link](0) # exit with code 0 (success)
# [Link](1) # exit with code 1 (error)
# Memory usage of object
import sys
x = [1, 2, 3, 4, 5]
print([Link](x)) # size in bytes (~120)
print([Link](42)) # size of int (~28)
# Recursion limit
print([Link]()) # 1000 (default)
[Link](5000) # increase if needed
# Platform
print([Link]) # 'linux', 'win32', 'darwin'
Chapter 23: math Module
Definition: The math module provides mathematical functions. It works with float values and is faster than
equivalent Python code for numerical computation.
Function/Constant Description Example
[Link] π = 3.14159... [Link] → 3.141592653589793
math.e Euler's number = 2.71828... math.e → 2.718281828459045
[Link] Positive infinity [Link] > 10**100 → True
[Link](x) Square root [Link](16) → 4.0
[Link](x,y) x to the power y (float) [Link](2,10) → 1024.0
[Link](x) Absolute value (use built-in abs) abs(-5) → 5
[Link](x) Round up [Link](4.1) → 5
[Link](x) Round down [Link](4.9) → 4
[Link](x) Round to nearest (built-in) round(4.5) → 4 (banker's)
[Link](n) n! [Link](5) → 120
[Link](a,b) Greatest common divisor [Link](12,8) → 4
[Link](a,b) Least common multiple (3.9+) [Link](4,6) → 12
[Link](x,base) Logarithm (default base e) [Link](100,10) → 2.0
math.log2(x) Base-2 logarithm math.log2(8) → 3.0
math.log10(x) Base-10 logarithm math.log10(1000) → 3.0
[Link](x) e^x [Link](1) → 2.718...
[Link]/cos/tan(x) Trig functions (radians) [Link]([Link]/2) → 1.0
[Link](x) Radians to degrees [Link]([Link]) → 180
[Link](x) Degrees to radians [Link](180) → π
[Link](x) True if not inf or nan [Link](1.5) → True
[Link](x) True if infinite [Link]([Link]) → True
[Link](x) True if NaN [Link](float('nan')) → True
[Link](x,y) √(x²+y²) — hypotenuse [Link](3,4) → 5.0
[Link](n,k) Combinations nCk (3.8+) [Link](5,2) → 10
[Link](n,k) Permutations nPk (3.8+) [Link](5,2) → 20
Chapter 24: datetime Module
Definition: The datetime module provides classes for working with dates and times. It supports parsing,
formatting, arithmetic (adding/subtracting time), and timezone handling.
▸ datetime Module — Complete Guide
from datetime import datetime, date, time, timedelta
# Current date and time
now = [Link]() # local time
today = [Link]() # just the date
print(now) # 2025-03-11 14:30:45.123456
print(today) # 2025-03-11
# Creating specific date/time
dt = datetime(2025, 12, 25, 8, 30, 0) # Christmas 8:30 AM
d = date(2025, 1, 1) # New Year's Day
t = time(10, 30, 45) # 10:30:45
# Accessing components
print([Link], [Link], [Link]) # 2025 12 25
print([Link], [Link], [Link]) # 8 30 0
print([Link]()) # 0=Mon...6=Sun
print([Link]('%A')) # Wednesday
# Formatting: strftime (datetime TO string)
print([Link]('%Y-%m-%d %H:%M:%S')) # 2025-03-11 14:30:45
print([Link]('%d/%m/%Y')) # 11/03/2025
print([Link]('%B %d, %Y')) # March 11, 2025
print([Link]('%I:%M %p')) # 02:30 PM
# Parsing: strptime (string TO datetime)
s = '2025-07-04 09:00:00'
dt2 = [Link](s, '%Y-%m-%d %H:%M:%S')
print(dt2) # 2025-07-04 09:00:00
# Date arithmetic with timedelta
delta = timedelta(days=30, hours=6)
future = now + delta
past = now - timedelta(weeks=2)
print(future)
# Difference between dates
birth = date(2000, 5, 15)
diff = today - birth
print([Link]) # number of days alive
print([Link] // 365) # approximate age in years
# Comparing dates
d1 = date(2025, 1, 1)
d2 = date(2025, 12, 31)
print(d1 < d2) # True
Chapter 25: collections Module
Definition (GFG): The collections module provides specialized container datatypes that extend Python's
built-in dict, list, set, and tuple. These are optimized for specific use cases.
25.1 Counter
▸ Counter
from collections import Counter
# Count elements in a sequence
text = 'hello world'
counter = Counter(text)
print(counter) # Counter({'l': 3, 'o': 2, 'h': 1, ...})
# Count words
words = ['apple','banana','apple','cherry','banana','apple']
word_count = Counter(words)
print(word_count) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
# Most common
print(word_count.most_common(2)) # [('apple',3), ('banana',2)]
# Arithmetic on Counters
c1 = Counter(['a','a','b','c'])
c2 = Counter(['a','b','b','d'])
print(c1 + c2) # Counter({'a':3,'b':3,'c':1,'d':1})
print(c1 - c2) # Counter({'a':1,'c':1}) — only positive
print(c1 & c2) # Counter({'a':1,'b':1}) — minimum
print(c1 | c2) # Counter({'a':2,'b':2,'c':1,'d':1}) — maximum
# Update counter
word_count.update(['apple', 'date'])
word_count.subtract(['banana']) # decrements (not removes)
25.2 defaultdict
▸ defaultdict
from collections import defaultdict
# defaultdict provides a default value for missing keys
# Regular dict raises KeyError; defaultdict creates new entry
# Group words by first letter
words = ['apple','avocado','banana','blueberry','cherry','cranberry']
grouped = defaultdict(list)
for word in words:
grouped[word[0]].append(word) # no KeyError!
print(dict(grouped))
# {'a': ['apple','avocado'], 'b': ['banana','blueberry'], ...}
# Count (equivalent to Counter but more flexible)
frequency = defaultdict(int) # default value is 0
for w in words:
frequency[w] += 1
# Nested defaultdict
matrix = defaultdict(lambda: defaultdict(int))
matrix['A']['B'] += 1
matrix['A']['C'] += 2
print(matrix['A']) # defaultdict(<class 'int'>, {'B': 1, 'C': 2})
# defaultdict with set
author_books = defaultdict(set)
books = [('Alice','Python'), ('Bob','Java'), ('Alice','C++')]
for author, book in books:
author_books[author].add(book)
print(dict(author_books))
# {'Alice': {'Python', 'C++'}, 'Bob': {'Java'}}
25.3 deque
▸ deque
from collections import deque
# Double-ended queue — O(1) append/pop from BOTH ends
# Regular list: insert/pop from left is O(n)
# deque: insert/pop from left is O(1)
dq = deque([1, 2, 3, 4, 5])
# Both ends
[Link](6) # add to right: [1,2,3,4,5,6]
[Link](0) # add to left: [0,1,2,3,4,5,6]
[Link]() # remove from right: 6
[Link]() # remove from left: 0
print(dq) # deque([1, 2, 3, 4, 5])
# Rotate
[Link](2) # shift right by 2: deque([4,5,1,2,3])
[Link](-2) # shift left by 2: back to deque([1,2,3,4,5])
# maxlen — sliding window
window = deque(maxlen=3) # keeps only last 3
for i in range(10):
[Link](i)
print(list(window))
# Common use: BFS (Breadth-First Search)
def bfs(graph, start):
visited = set()
queue = deque([start])
while queue:
node = [Link]() # O(1) dequeue
if node not in visited:
[Link](node)
[Link]([Link](node, []))
return visited
25.4 OrderedDict, namedtuple, ChainMap
▸ OrderedDict, namedtuple, ChainMap
from collections import OrderedDict, namedtuple, ChainMap
# OrderedDict — preserves insertion order (Python 3.7+ dicts already do this)
# Key advantage: move_to_end() and order-sensitive equality
od = OrderedDict()
od['first'] = 1
od['second'] = 2
od['third'] = 3
od.move_to_end('first') # move to end
od.move_to_end('third', last=False) # move to front
print(list([Link]())) # ['third', 'second', 'first']
# namedtuple — tuple with named fields
Point = namedtuple('Point', ['x', 'y'])
Person = namedtuple('Person', ['name', 'age', 'email'])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p[0], p[1]) # 3 4 (index access still works)
alice = Person('Alice', 30, 'alice@[Link]')
print([Link]) # Alice
print(alice._asdict()) # OrderedDict or dict
alice2 = alice._replace(age=31) # create modified copy
# ChainMap — combine multiple dicts, search in order
defaults = {'color': 'blue', 'size': 'medium', 'font': 'Arial'}
user_pref = {'color': 'red', 'size': 'large'}
app_config = {'color': 'green'}
# searches in order: app_config → user_pref → defaults
config = ChainMap(app_config, user_pref, defaults)
print(config['color']) # green (from app_config)
print(config['font']) # Arial (from defaults)
Chapter 26: itertools Module
Definition: The itertools module provides functions for creating iterators for efficient looping. These are lazy
(memory-efficient) — they generate values on demand. Used extensively in algorithms, data processing, and
combinatorics.
▸ itertools Module
import itertools
# count(start, step) — infinite counter
counter = [Link](1, 2) # 1, 3, 5, 7, 9...
print(list([Link](counter, 5))) # [1, 3, 5, 7, 9]
# cycle(iterable) — repeat forever
traffic = [Link](['red','yellow','green'])
lights = [next(traffic) for _ in range(7)]
# ['red','yellow','green','red','yellow','green','red']
# repeat(value, n) — repeat n times
print(list([Link](42, 4))) # [42, 42, 42, 42]
# chain(*iterables) — concatenate iterables
print(list([Link]([1,2], [3,4], [5,6]))) # [1,2,3,4,5,6]
print(list([Link].from_iterable([[1,2],[3,4]]))) # same
# islice(iter, stop) — slice an iterator
print(list([Link](range(100), 5, 15, 2))) # [5,7,9,11,13]
# combinations, permutations
cards = ['A','K','Q']
print(list([Link](cards, 2)))
# [('A','K'), ('A','Q'), ('K','Q')] — no repeats, order matters for perms
print(list([Link](cards, 2)))
# [('A','K'),('A','Q'),('K','A'),('K','Q'),('Q','A'),('Q','K')]
print(list(itertools.combinations_with_replacement('AB', 2)))
# [('A','A'),('A','B'),('B','B')]
# product — cartesian product
print(list([Link]([1,2], ['a','b'])))
# [(1,'a'),(1,'b'),(2,'a'),(2,'b')]
# groupby — group consecutive identical keys
data = [1,1,2,2,2,3,1,1]
for key, grp in [Link](data):
print(key, list(grp)) # 1 [1,1], 2 [2,2,2], 3 [3], 1 [1,1]
# accumulate — running totals
nums = [1, 2, 3, 4, 5]
print(list([Link](nums))) # [1,3,6,10,15]
print(list([Link](nums, max))) # [1,2,3,4,5] running max
Chapter 27: functools Module
▸ functools Module
import functools
# reduce(function, iterable) — fold left
from functools import reduce
product = reduce(lambda x, y: x*y, [1,2,3,4,5]) # 120 = 5!
print(product)
# partial — fix some arguments of a function
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2) # fix exp=2
cube = partial(power, exp=3) # fix exp=3
print(square(5)) # 25
print(cube(3)) # 27
# lru_cache — memoization (cache function results)
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2: return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(50)) # 12586269025 — instant! (without cache: SLOW)
print(fibonacci.cache_info()) # hits, misses, maxsize, currsize
# cache (Python 3.9+) — lru_cache with no size limit
from functools import cache
@cache
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
# total_ordering — define < and ==, get rest for free
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, gpa):
[Link], [Link] = name, gpa
def __eq__(self, other):
return [Link] == [Link]
def __lt__(self, other): # only need < and ==
return [Link] < [Link]
a = Student('Alice', 3.9)
b = Student('Bob', 3.5)
print(a > b) # True (automatically derived!)
print(a >= b) # True (automatically derived!)
Chapter 28: re (Regular Expressions) Module
Definition (GFG): Regular expressions (regex) are patterns used to match, search, and manipulate strings. The
re module provides functions for working with regex. Regex is essential for text processing, validation, and data
extraction.
Pattern What It Matches Example
. Any character except newline 'h.t' → 'hit', 'hat', 'hot'
^ Start of string '^Hello' → matches if starts with Hello
$ End of string 'world$' → matches if ends with world
* 0 or more of previous 'ab*' → 'a', 'ab', 'abb', 'abbb'
+ 1 or more of previous 'ab+' → 'ab', 'abb', 'abbb'
? 0 or 1 of previous 'colou?r' → 'color' or 'colour'
{n} Exactly n repetitions '\\d{3}' → exactly 3 digits
{n,m} Between n and m repetitions '\\d{2,4}' → 2 to 4 digits
[abc] Any one of a, b, c '[aeiou]' → any vowel
[^abc] Any character EXCEPT a,b,c '[^0-9]' → non-digit
[a-z] Any char in range a to z '[A-Za-z]' → any letter
\\d Digit [0-9] '\\d+' → one or more digits
\\D Non-digit '\\D+' → one or more non-digits
\\w Word char [a-zA-Z0-9_] '\\w+' → a word
\\W Non-word character '\\W+' → spaces, punctuation
\\s Whitespace '\\s+' → spaces/tabs/newlines
\\S Non-whitespace '\\S+' → non-space
\\b Word boundary '\\bcat\\b' → 'cat' not 'concatenate'
(...) Capture group '(\\d+)-(\\w+)' → captures each part
(?:...) Non-capture group Groups without capturing
a|b Matches a or b 'cat|dog' → 'cat' or 'dog'
▸ re Module — Regular Expressions
import re
text = 'My phone is 555-1234 and backup is 555-5678'
# [Link]() — find first match anywhere in string
m = [Link](r'\d{3}-\d{4}', text)
if m:
print([Link]()) # 555-1234
print([Link]()) # 14 (start index)
print([Link]()) # 22 (end index)
# [Link]() — find ALL matches, return as list
phones = [Link](r'\d{3}-\d{4}', text)
print(phones) # ['555-1234', '555-5678']
# [Link]() — match only at START of string
m = [Link](r'My', text)
print(bool(m)) # True
# [Link]() — match ENTIRE string
m = [Link](r'\d{4}-\d{2}-\d{2}', '2025-03-11')
print(bool(m)) # True
# [Link]() — replace matches
result = [Link](r'\d{3}-\d{4}', 'XXX-XXXX', text)
print(result) # 'My phone is XXX-XXXX and backup is XXX-XXXX'
# [Link]() — split on pattern
parts = [Link](r'[,;\s]+', 'one, two; three four')
print(parts) # ['one', 'two', 'three', 'four']
# Groups — extract specific parts
email = 'user@[Link]'
m = [Link](r'(\w+)@(\w+)\.(\w+)', email)
if m:
print([Link](0)) # user@[Link] (full match)
print([Link](1)) # user
print([Link](2)) # example
print([Link](3)) # com
print([Link]()) # ('user', 'example', 'com')
# Compiled pattern (faster for repeated use)
phone_re = [Link](r'\d{3}-\d{4}')
phones2 = phone_re.findall(text)
# Flags
# [Link] (re.I) — case-insensitive
# [Link] (re.M) — ^ and $ match line start/end
# [Link] (re.S) — . matches newline too
m = [Link](r'my', text, [Link]) # finds 'My'
Chapter 29: json Module
Definition: JSON (JavaScript Object Notation) is a lightweight data format used for data exchange. The json
module converts between Python objects and JSON strings. JSON is text-based and human-readable.
Python Type JSON Equivalent
dict {...} — object
list, tuple [...] — array
str "string"
int, float number
True / False true / false
None null
▸ json Module
import json
# Python dict → JSON string
data = {
'name': 'Alice',
'age': 30,
'skills': ['Python', 'SQL', 'ML'],
'active': True,
'score': 98.5
}
json_str = [Link](data) # compact
json_pretty = [Link](data, indent=4) # formatted
print(json_str)
# {"name": "Alice", "age": 30, ...}
# JSON string → Python dict
restored = [Link](json_str)
print(type(restored)) # <class 'dict'>
print(restored['name']) # Alice
# Write to file
with open('[Link]', 'w') as f:
[Link](data, f, indent=4)
# Read from file
with open('[Link]', 'r') as f:
loaded = [Link](f)
# Sorting keys
sorted_json = [Link](data, sort_keys=True, indent=2)
# Custom encoder for non-serializable types
from datetime import datetime
class DateEncoder([Link]):
def default(self, obj):
if isinstance(obj, datetime):
return [Link]()
return super().default(obj)
event = {'name': 'Meeting', 'time': [Link]()}
print([Link](event, cls=DateEncoder))
Chapter 30: pathlib Module
Definition (Python 3.4+): The pathlib module provides object-oriented filesystem paths. It is more readable
and intuitive than [Link]. Paths are objects with methods and can be combined using the / operator.
▸ pathlib Module
from pathlib import Path
# Creating paths
p = Path('.') # current directory
p = Path('/home/user/docs/[Link]')
p = [Link]() # home directory
p = [Link]() # current working directory
# Path components
p = Path('/home/user/docs/[Link]')
print([Link]) # '[Link]'
print([Link]) # 'report'
print([Link]) # '.pdf'
print([Link]) # /home/user/docs
print([Link][0]) # /home/user/docs
print([Link][1]) # /home/user
# Combining paths with /
base = Path('/home/user')
new = base / 'projects' / 'python' / '[Link]'
print(new) # /home/user/projects/python/[Link]
# Checking
print([Link]()) # True/False
print(p.is_file()) # True
print(p.is_dir()) # False
# Reading/Writing
text_file = Path('[Link]')
text_file.write_text('Hello, World!') # write
content = text_file.read_text() # read
bytes_data = text_file.read_bytes() # as bytes
# Creating dirs
new_dir = Path('output/results')
new_dir.mkdir(parents=True, exist_ok=True) # creates all parents
# Listing files
for f in Path('.').iterdir():
print([Link])
# Glob patterns
python_files = list(Path('.').glob('*.py'))
all_py = list(Path('.').rglob('*.py')) # recursive
# Rename / move
text_file.rename('new_name.txt')
text_file.replace('backup/[Link]') # move
PART 12
ADVANCED PYTHON — GENERATORS, COMPREHENSIONS, FILES
Chapter 31: Iterators and Generators
31.1 Iterator Protocol
Definition: An iterator is any object that implements __iter__() (returns itself) and __next__() (returns
next value or raises StopIteration). Every for loop in Python uses the iterator protocol internally.
▸ Iterator Protocol
# Under the hood of 'for x in [1,2,3]'
lst = [1, 2, 3]
it = iter(lst) # calls lst.__iter__()
print(next(it)) # 1 — calls it.__next__()
print(next(it)) # 2
print(next(it)) # 3
# print(next(it)) # StopIteration!
# Custom iterator class
class Range:
'''Custom range-like iterator'''
def __init__(self, start, stop, step=1):
[Link] = start
[Link] = stop
[Link] = step
def __iter__(self):
return self # iterator is its own iterable
def __next__(self):
if [Link] >= [Link]:
raise StopIteration
val = [Link]
[Link] += [Link]
return val
r = Range(1, 10, 2)
print(list(r)) # [1, 3, 5, 7, 9]
for n in Range(0, 6):
print(n, end=' ') # 0 1 2 3 4 5
31.2 Generators
Definition (GFG): A generator is a function that uses yield to produce a series of values lazily. It pauses
execution at each yield and resumes when next() is called. Generators are extremely memory-efficient —
they never store all values at once.
▸ Generators — Complete Guide
# Simple generator function
def countdown(n):
print('Starting countdown')
while n > 0:
yield n # pause here, return n
n -= 1 # resume here next time
print('Liftoff!')
gen = countdown(3)
print(type(gen)) # <class 'generator'>
print(next(gen)) # Starting countdown → 3
print(next(gen)) # 2
print(next(gen)) # 1
# next(gen) # Liftoff! then StopIteration
# Generator for infinite Fibonacci sequence
def fibonacci():
a, b = 0, 1
while True: # infinite!
yield a
a, b = b, a+b
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]
print(first_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Memory comparison
import sys
list_version = [x**2 for x in range(10000)] # list
gen_version = (x**2 for x in range(10000)) # generator
print([Link](list_version)) # ~85000 bytes
print([Link](gen_version)) # 120 bytes
# Generator pipeline
def read_large_file(path):
with open(path) as f:
for line in f: # reads ONE line at a time
yield [Link]()
def filter_empty(lines):
for line in lines:
if line: // skip empty
yield line
def uppercase(lines):
for line in lines:
yield [Link]()
# Chain generators — processes line by line, not all at once
# pipeline = uppercase(filter_empty(read_large_file('[Link]')))
# for line in pipeline:
# print(line)
31.3 All Four Comprehensions
▸ All Four Comprehensions
# 1. LIST COMPREHENSION: [expr for item in iter if cond]
squares = [x**2 for x in range(1, 11)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# With multiple conditions
fizzbuzz = ['FizzBuzz' if x%15==0 else 'Fizz' if x%3==0
else 'Buzz' if x%5==0 else str(x)
for x in range(1, 16)]
# 2. DICT COMPREHENSION: {k: v for ...}
word_len = {w: len(w) for w in ['hi','hello','hey']}
# {'hi': 2, 'hello': 5, 'hey': 3}
# Swap keys and values
original = {'a': 1, 'b': 2, 'c': 3}
swapped = {v: k for k, v in [Link]()}
# {1: 'a', 2: 'b', 3: 'c'}
# Filter dict
scores = {'Alice': 95, 'Bob': 72, 'Carol': 88, 'Dave': 65}
passing = {k: v for k, v in [Link]() if v >= 75}
# {'Alice': 95, 'Carol': 88}
# 3. SET COMPREHENSION: {expr for ...}
unique_lengths = {len(w) for w in ['cat','dog','elephant','ant']}
# {3, 8}
# Remove duplicates keeping only unique values
data = [1, -1, 2, -2, 3, -3]
unique_abs = {abs(x) for x in data}
# {1, 2, 3}
# 4. GENERATOR EXPRESSION: (expr for ...)
gen = (x**2 for x in range(1000000)) # no memory used!
print(sum(gen)) # compute sum lazily
# Use in function calls
total = sum(x**2 for x in range(1, 11))
maximum = max(len(w) for w in ['apple','banana','cherry'])
any_negative = any(x < 0 for x in [1, 2, -3, 4])
all_positive = all(x > 0 for x in [1, 2, 3, 4])
Chapter 32: File I/O — Complete Guide
Definition: File I/O allows reading from and writing to files. Python uses the built-in open() function which
returns a file object. Always use with (context manager) to ensure files are automatically closed.
▸ File I/O — Complete Guide
# File modes
# 'r' — read (default). File must exist.
# 'w' — write. Creates file or OVERWRITES existing.
# 'a' — append. Creates file or appends to existing.
# 'x' — exclusive create. Fails if file exists.
# 'r+' — read and write.
# 'rb' — read binary (images, PDFs, executables)
# 'wb' — write binary
# Writing
with open('[Link]', 'w') as f:
[Link]('First line\n')
[Link]('Second line\n')
[Link](['Line 3\n', 'Line 4\n']) # write list
# Reading — entire file
with open('[Link]', 'r') as f:
content = [Link]() # entire file as one string
print(len(content)) # character count
# Reading — line by line (memory efficient for large files)
with open('[Link]') as f: # 'r' is default
for line in f: # iterate line by line
print([Link]()) # strip removes \n
# Reading — specific methods
with open('[Link]') as f:
first = [Link]() # one line
rest = [Link]() # remaining lines as list
# Append to existing file
with open('[Link]', 'a') as f:
[Link]('Appended line\n')
# Binary files (images, etc.)
with open('[Link]', 'rb') as f:
img_data = [Link]()
with open('[Link]', 'wb') as f:
[Link](img_data)
# File cursor operations
with open('[Link]') as f:
[Link](5) # read 5 chars
print([Link]()) # current position
[Link](0) # go back to start
print([Link](5)) # read again from start
# Working with CSV
import csv
data = [['Name','Age','City'],['Alice',30,'NYC'],['Bob',25,'LA']]
with open('[Link]', 'w', newline='') as f:
writer = [Link](f)
[Link](data)
with open('[Link]') as f:
reader = [Link](f)
for row in reader:
print(row)
PART 13
STRING METHODS — COMPLETE REFERENCE (47 METHODS)
Chapter 33: All String Methods
Python strings have 47 built-in methods. All return new strings — strings are immutable, so no method modifies
the original.
33.1 Case Conversion Methods
Method Description Example Output
upper() All characters UPPERCASE 'hello World'.upper() 'HELLO WORLD'
lower() All characters lowercase 'HELLO World'.lower() 'hello world'
capitalize() First char upper, rest lower 'hELLO wORLD'.capitalize() 'Hello world'
title() First char of each word upper 'hello world'.title() 'Hello World'
swapcase() Swap upper↔lower each char 'Hello World'.swapcase() 'hELLO wORLD'
casefold() Aggressive lowercase (Unicode- 'Straße'.casefold() 'strasse'
aware)
33.2 Search & Check Methods
Method Description Returns Example
find(sub,start,end) First index of sub (-1 if not found) int 'hello'.find('l') → 2
rfind(sub) Last index of sub (-1 if not found) int 'hello'.rfind('l') → 3
index(sub) Like find but raises ValueError int 'hello'.index('e') → 1
rindex(sub) Like rfind but raises ValueError int 'hello'.rindex('l') → 3
count(sub) Non-overlapping occurrences int 'banana'.count('an') → 2
startswith(prefix) Check beginning bool 'hello'.startswith('he') → True
endswith(suffix) Check ending bool 'hello'.endswith('lo') → True
in (operator) Membership test bool 'ell' in 'hello' → True
33.3 Modification Methods
Method Description Example Output
strip(chars) Remove leading+trailing chars ' hi '.strip() 'hi'
(default: whitespace)
lstrip(chars) Remove leading chars 'xxhixx'.lstrip('x') 'hixx'
rstrip(chars) Remove trailing chars 'xxhixx'.rstrip('x') 'xxhi'
replace(old,new,n) Replace occurrences (n=max 'aabbcc'.replace('b','X') 'aaXXcc'
count)
removeprefix(p) Remove prefix (Python 3.9+) 'HelloWorld'.removeprefix('Hel 'World'
lo')
removesuffix(s) Remove suffix (Python 3.9+) 'HelloWorld'.removesuffix('Wo 'Hello'
rld')
33.4 Split & Join Methods
Method Description Example Output
split(sep,maxsplit) Split by sep into list 'a,b,c'.split(',') ['a','b','c']
rsplit(sep,maxsplit) Split from right 'a.b.c'.rsplit('.',1) ['a.b','c']
splitlines(keepends) Split on line boundaries 'a\nb\nc'.splitlines() ['a','b','c']
join(iterable) Join iterable with self as ','.join(['a','b','c']) 'a,b,c'
separator
partition(sep) Split into 3: before, sep, after 'hello'.partition('l') ('he','l','lo')
rpartition(sep) Like partition from right 'hello'.rpartition('l') ('hel','l','o')
33.5 Padding & Formatting
Method Description Example Output
center(w,fillchar) Center in width w with fill char 'Hi'.center(10,'*') '****Hi****'
ljust(w,fillchar) Left-align in width w 'Hi'.ljust(10,'.') 'Hi........'
rjust(w,fillchar) Right-align in width w 'Hi'.rjust(10,'.') '........Hi'
zfill(width) Pad with zeros on left '42'.zfill(6) '000042'
expandtabs(tabsize) Replace \t with spaces 'a\tb'.expandtabs(4) 'a b'
format(*args,**kwargs) Format with placeholders '{0} {1}'.format('Hi','World') 'Hi World'
format_map(mapping) Format with mapping '{x}'.format_map({'x':5}) '5'
33.6 Validation Methods (is-checks)
Method Returns True If... Example
isalpha() All chars are alphabetic letters 'Hello'.isalpha() → True
isdigit() All chars are digits 0-9 '123'.isdigit() → True
isnumeric() All chars are numeric (includes ½, ² etc) '½'.isnumeric() → True
isdecimal() All chars are decimal digits only '123'.isdecimal() → True
isalnum() All chars are alphanumeric 'abc123'.isalnum() → True
isspace() All chars are whitespace ' \t\n'.isspace() → True
isupper() All cased chars are uppercase 'ABC'.isupper() → True
islower() All cased chars are lowercase 'abc'.islower() → True
istitle() Title-cased (each word starts with 'Hi World'.istitle() → True
upper)
isidentifier() Valid Python identifier 'my_var'.isidentifier() → True
isprintable() All chars are printable 'Hello!'.isprintable() → True
isascii() All chars are ASCII (Python 3.7+) 'Hello'.isascii() → True
33.7 Encoding & Other Methods
Method Description Example
encode(encoding) Return bytes object 'Hello'.encode('utf-8') → b'Hello'
maketrans(x,y,z) Create translation table (static) [Link]('aeiou','12345')
translate(table) Apply translation table 'hello'.translate(table)
__contains__(sub) in operator 'ell' in 'hello' → True
__len__() len() function len('hello') → 5
__getitem__(i) Index access 'hello'[0] → 'h'
__iter__() Iteration for c in 'abc': ...
📚 Quick Reference Summary
Part 1–4: Python basics, installation, variables, I/O, data types, strings
Part 5: All 8 operator types + walrus := + precedence rules
Part 6: if/elif/else, ternary, match-case, for/while loops, break/continue
Part 7: Functions, LEGB scope, lambda, recursion, decorators
Part 8: Exception handling, hierarchy, try/except/finally, custom exceptions
Part 9: Lists, Tuples, Sets, Dictionaries — all methods and operations
Part 10: OOP — classes, encapsulation, inheritance, polymorphism, abstraction, dunders
Part 11: os, sys, math, datetime, collections, itertools, functools, re, json, pathlib
Part 12: Iterators, generators, comprehensions, file I/O
Part 13: All 47 string methods complete reference
PART A
OBJECT-ORIENTED PROGRAMMING — ULTIMATE DEEP DIVE
Introduction to OOP
What is OOP? Object-Oriented Programming is a way of designing programs by modeling real-world things as
'objects'. An object is a combination of data (attributes/state) and behavior (methods/functions). Instead of
writing a program as a sequence of instructions, OOP groups related data and functions together into a single
unit called a class.
Procedural vs OOP: In procedural code, functions and data are separate. If you want to model a bank account
procedurally, you pass balance around as a variable. In OOP, the account is an object that knows its own balance
and knows how to deposit/withdraw — the data and its operations are always together.
The Four Pillars of OOP — Detailed
1. ENCAPSULATION: Bundling data + methods together AND restricting direct access to internals. Like a
capsule — medicine inside is hidden from you. You take the pill; you don't handle the chemicals directly.
2. ABSTRACTION: Hiding complex implementation, exposing only what the user needs. Like driving a car
— you use a steering wheel/pedals without knowing how the engine works.
3. INHERITANCE: A child class inherits all properties and behaviors of the parent class and can
add/override them. Like a child inheriting traits from parents.
4. POLYMORPHISM: Same method name, different behaviors in different classes. Like a 'speak()' method
— humans speak in sentences, dogs bark, cats meow.
Section 1: Classes and Objects — Deep Dive
1.1 What Is a Class?
Class: A class is a blueprint / template / cookie-cutter that defines what data an object will hold and what it can
do. The class itself is NOT an object — it is the definition. Think of a class as an architectural blueprint for a
house. The blueprint is not a house; you build houses FROM blueprints.
Object / Instance: An object is a concrete realization of a class created in memory. You can create many objects
from one class, each with its own independent data. Every object built from the same class shares the same
methods but has its own copy of instance variables.
self: Inside a class, self is a reference to the specific instance calling the method. When you call
[Link](), Python automatically passes the dog object as self. You must declare self as the first
parameter of every instance method, but you don't pass it manually.
▸ Classes and Objects — Full Example
# Anatomy of a class
class Dog:
# CLASS VARIABLE: shared by ALL instances
species = 'Canis lupus familiaris'
count = 0
# CONSTRUCTOR: called automatically when Dog() is created
def __init__(self, name, breed, age):
# INSTANCE VARIABLES: unique to each Dog object
[Link] = name
[Link] = breed
[Link] = age
[Link] = [] # each dog has its own list
[Link] += 1 # increment class variable
# INSTANCE METHOD: operates on a specific dog
def bark(self):
return f'{[Link]} says: Woof!'
def learn_trick(self, trick):
[Link](trick)
return f'{[Link]} learned {trick}!'
def show_tricks(self):
if not [Link]:
return f'{[Link]} knows no tricks yet'
return f'{[Link]} knows: {', '.join([Link])}'
def birthday(self):
[Link] += 1
return f'Happy birthday {[Link]}! Now {[Link]} years old.'
def __str__(self):
return f'Dog(name={[Link]}, breed={[Link]}, age={[Link]})'
# Creating objects (instances)
rex = Dog('Rex', 'German Shepherd', 3)
buddy = Dog('Buddy', 'Golden Retriever', 5)
max_ = Dog('Max', 'Labrador', 2)
# Each object is INDEPENDENT
rex.learn_trick('sit')
rex.learn_trick('shake')
buddy.learn_trick('fetch')
print([Link]()) # Rex says: Woof!
print(buddy.show_tricks()) # Buddy knows: fetch
print(rex.show_tricks()) # Rex knows: sit, shake
# Accessing attributes
print([Link]) # Rex (instance attr)
print([Link]) # Canis lupus familiaris (class attr)
print([Link]) # 3 (via class)
# Modifying attribute directly
[Link] = 'Rexxy'
print(rex) # Dog(name=Rexxy, breed=German Shepherd, age=3)
# type(), isinstance(), id()
print(type(rex)) # <class '__main__.Dog'>
print(isinstance(rex, Dog)) # True
print(id(rex) == id(buddy)) # False (different objects in memory)
1.2 Class Variables vs Instance Variables
Key Rule: Instance variables are unique per object. Class variables are shared across ALL instances. Warning: If
you assign self.class_var = x inside a method, Python creates a new instance variable that shadows the
class variable for that object — it does NOT modify the class variable!
▸ Class vs Instance Variables
class Counter:
# Class variable — SHARED
total = 0
instances = []
def __init__(self, name):
# Instance variables — UNIQUE
[Link] = name
[Link] = 0
[Link] += 1
[Link](self)
def increment(self, by=1):
[Link] += by
# Create instances
c1 = Counter('Alpha')
c2 = Counter('Beta')
c3 = Counter('Gamma')
[Link](5)
[Link](10)
[Link](3)
print([Link]) # 3
print([[Link] for c in [Link]]) # ['Alpha', 'Beta', 'Gamma']
# Danger: shadowing class variable
class Conf:
setting = 'global'
a = Conf()
b = Conf()
[Link] = 'local' # creates INSTANCE var on 'a', does NOT change class var
print([Link]) # 'local' (instance var shadows class var)
print([Link]) # 'global' (class var unchanged)
print([Link]) # 'global' (class var unchanged)
1.3 Class Methods and Static Methods
▸ Class, Instance, Static Methods
class Employee:
company = 'TechCorp'
_employees = []
def __init__(self, name, dept, salary):
[Link] = name
[Link] = dept
[Link] = salary
Employee._employees.append(self)
# ── INSTANCE METHOD ── needs 'self', accesses instance data
def give_raise(self, pct):
[Link] *= (1 + pct/100)
return f'{[Link]} new salary: {[Link]:.2f}'
# ── CLASS METHOD ── needs 'cls', acts on class-level data
@classmethod
def from_string(cls, emp_str):
'''Factory: create Employee from 'Name-Dept-Salary' string'''
name, dept, salary = emp_str.split('-')
return cls(name, dept, float(salary)) # cls() == Employee()
@classmethod
def headcount(cls):
return len(cls._employees)
@classmethod
def dept_report(cls, dept):
staff = [e for e in cls._employees if [Link] == dept]
return f'{dept}: {len(staff)} employees'
# ── STATIC METHOD ── no 'self' or 'cls'; standalone utility
@staticmethod
def is_valid_salary(salary):
return isinstance(salary, (int, float)) and 10000 <= salary <=
10_000_000
@staticmethod
def format_currency(amount, symbol='₹'):
return f'{symbol}{amount:,.2f}'
def __repr__(self):
return f'Employee({[Link]!r}, {[Link]!r}, {[Link]})'
# Using class methods
e1 = Employee('Alice', 'Engineering', 80000)
e2 = Employee.from_string('Bob-Marketing-60000') # factory method
e3 = Employee.from_string('Carol-Engineering-90000')
print([Link]()) # 3
print(Employee.dept_report('Engineering')) # Engineering: 2 employees
print(e1.give_raise(10)) # Alice new salary: 88000.00
# Using static methods
print(Employee.is_valid_salary(80000)) # True
print(Employee.is_valid_salary(-500)) # False
print(Employee.format_currency(88000)) # ₹88,000.00
# Static methods can also be called on instances (but better on class)
print(e1.is_valid_salary(80000)) # True (works, but unconventional)
Section 2: Encapsulation — Access Control & Properties
Definition: Encapsulation has two aspects: (1) Grouping — keep related data and methods together in a class.
(2) Data hiding — restrict access to internal state to prevent accidental modification. Python doesn't enforce
private access (unlike Java/C++), but uses naming conventions.
Convention Syntax Meaning Accessible From
Public [Link] Accessible everywhere Anywhere
Protected self._name By convention: internal use only Class + subclasses (not enforced)
Private self.__name Name-mangled to Only inside the class (mangled)
_ClassName__name
▸ Encapsulation with Properties
class BankAccount:
def __init__(self, owner, balance):
[Link] = owner # public
self._bank_code = 'BNK001' # protected (internal use)
self.__balance = balance # private (mangled →
_BankAccount__balance)
self.__pin = None
self.__history = []
# ── @property ── makes method look like an attribute
@property
def balance(self):
return self.__balance
# ── @[Link] ── validates BEFORE assignment
@[Link]
def balance(self, amount):
raise AttributeError('Direct balance assignment not allowed. Use
deposit/withdraw.')
@property
def masked_pin(self):
return '****' if self.__pin else 'Not set'
def set_pin(self, pin):
if not isinstance(pin, int) or not (1000 <= pin <= 9999):
raise ValueError('PIN must be a 4-digit integer')
self.__pin = pin
print('PIN set successfully')
def deposit(self, amount):
if amount <= 0:
raise ValueError('Deposit must be positive')
self.__balance += amount
self.__history.append(f'+{amount}')
def withdraw(self, amount, pin):
if self.__pin is None:
raise RuntimeError('Set a PIN first')
if pin != self.__pin:
raise PermissionError('Incorrect PIN')
if amount > self.__balance:
raise ValueError('Insufficient funds')
self.__balance -= amount
self.__history.append(f'-{amount}')
return amount
@property
def history(self):
return list(self.__history) # return COPY, not original
# Usage
acc = BankAccount('Alice', 1000)
acc.set_pin(1234)
[Link](500)
[Link](200, pin=1234)
print([Link]) # 1300 (via @property)
print(acc.masked_pin) # ****
print([Link]) # ['+500', '-200']
# Attempting to access private attribute directly
# print(acc.__balance) → AttributeError
# But name-mangled access still technically works (bad practice):
print(acc._BankAccount__balance) # 1300 (works but WRONG to do this)
💡 @property — Why Use It?
Without @property, you'd call acc.get_balance() — method syntax.
With @property, you call [Link] — cleaner attribute syntax.
Benefit: External code uses [Link]. Later you can add validation/logic in the getter/setter WITHOUT
changing any external code.
This is the Open/Closed Principle: open for extension, closed for modification.
Section 3: Inheritance — Complete Guide
Definition: Inheritance is a mechanism where a child class (subclass) acquires attributes and methods from a
parent class (superclass). The child class can: (1) Use inherited methods as-is. (2) Override them with new
behavior. (3) Extend them using super().
3.1 Single Inheritance
▸ Single Inheritance
# Base class (Parent / Superclass)
class Vehicle:
def __init__(self, make, model, year, fuel_type):
[Link] = make
[Link] = model
[Link] = year
self.fuel_type = fuel_type
[Link] = 0
self.is_running = False
def start(self):
self.is_running = True
return f'{[Link]} started'
def stop(self):
[Link] = 0
self.is_running = False
return f'{[Link]} stopped'
def accelerate(self, amount):
if not self.is_running:
return 'Vehicle not running'
[Link] += amount
return f'{[Link]} speed: {[Link]} km/h'
def info(self):
return f'{[Link]} {[Link]} {[Link]} ({self.fuel_type})'
def __str__(self):
return [Link]()
# Derived class (Child / Subclass)
class Car(Vehicle): # Car inherits from Vehicle
def __init__(self, make, model, year, num_doors=4):
# super().__init__() calls PARENT constructor
super().__init__(make, model, year, 'Petrol')
self.num_doors = num_doors # Car-specific attribute
[Link] = 1
# NEW method (unique to Car)
def shift_gear(self, gear):
if not self.is_running:
return 'Cannot shift — engine off'
[Link] = gear
return f'Shifted to gear {gear}'
# OVERRIDE parent method — enhance it
def accelerate(self, amount):
result = super().accelerate(amount) # call parent logic
if [Link] > 100:
return result + ' [WARNING: High speed!]'
return result
class Truck(Vehicle):
def __init__(self, make, model, year, payload_tons):
super().__init__(make, model, year, 'Diesel')
self.payload_tons = payload_tons
[Link] = []
def load(self, item):
[Link](item)
return f'Loaded: {item}. Cargo: {[Link]}'
# Override to include cargo context
def info(self):
base = super().info()
return f'{base} — Payload: {self.payload_tons}t'
# Testing
car = Car('Toyota', 'Corolla', 2023)
truck = Truck('Tata', 'Prima', 2022, 25)
[Link]()
print([Link](50)) # Toyota Corolla speed: 50 km/h
print([Link](60)) # ... [WARNING: High speed!]
print(car.shift_gear(4))
[Link]()
[Link]('Electronics')
print([Link]()) # 2022 Tata Prima (Diesel) — Payload: 25t
# Inheritance check
print(isinstance(car, Car)) # True
print(isinstance(car, Vehicle)) # True — car IS-A vehicle
print(issubclass(Car, Vehicle)) # True
print(Car.__bases__) # (<class '__main__.Vehicle'>,)
3.2 super() — Why and How
super() explained: super() returns a proxy object that delegates method calls to the parent class. It is smarter
than calling Parent.__init__(self) directly because: (1) You don't hardcode the parent class name. (2) It
correctly handles multiple inheritance using MRO.
▸ super() in depth
# super() in action
class A:
def __init__(self):
print('A.__init__')
self.a = 'A'
class B(A):
def __init__(self):
super().__init__() # calls A.__init__
print('B.__init__')
self.b = 'B'
class C(B):
def __init__(self):
super().__init__() # calls B.__init__ (which calls A.__init__)
print('C.__init__')
self.c = 'C'
obj = C()
# Output:
# A.__init__
# B.__init__
# C.__init__
print(obj.a, obj.b, obj.c) # A B C — all attributes available!
# super() calling a method (not just __init__)
class Animal:
def sound(self):
return 'Some sound'
class Dog(Animal):
def sound(self):
parent_sound = super().sound()
return f'Dog overrides [{parent_sound}] with: Woof!'
d = Dog()
print([Link]()) # Dog overrides [Some sound] with: Woof!
3.3 Multiple Inheritance and MRO
MRO — Method Resolution Order: When a class inherits from multiple parents, Python uses the C3
Linearization Algorithm to determine the order in which parent classes are searched for methods. You can view
MRO with ClassName.__mro__ or [Link]().
▸ Multiple Inheritance & MRO
# Multiple Inheritance
class Flyable:
def __init__(self):
self.max_altitude = 10000
def fly(self):
return f'Flying at up to {self.max_altitude}m'
def describe(self):
return 'I can fly'
class Swimmable:
def __init__(self):
self.max_depth = 50
def swim(self):
return f'Swimming up to {self.max_depth}m deep'
def describe(self):
return 'I can swim'
class Duck(Flyable, Swimmable):
def __init__(self, name):
Flyable.__init__(self) # explicit when super() is ambiguous
Swimmable.__init__(self)
[Link] = name
def quack(self):
return f'{[Link]}: Quack!'
def describe(self):
return f'{[Link]}: I can fly AND swim'
donald = Duck('Donald')
print([Link]()) # Flying at up to 10000m
print([Link]()) # Swimming up to 50m deep
print([Link]()) # Donald: Quack!
print([Link]()) # Donald's own describe() called
# MRO
print(Duck.__mro__)
# Duck → Flyable → Swimmable → object
# Diamond Problem — Python solves with MRO
class A:
def greet(self): return 'Hello from A'
class B(A):
def greet(self): return 'Hello from B'
class C(A):
def greet(self): return 'Hello from C'
class D(B, C): # inherits from B and C, both inherit from A
pass
d = D()
print([Link]()) # 'Hello from B' — B searched before C
print(D.__mro__) # D → B → C → A → object
3.4 Mixin Classes
Mixin: A mixin is a class designed to be 'mixed in' to other classes to add optional functionality. Mixins don't
make sense on their own — they're meant to supplement other classes. They're a clean way to share behaviors
without deep inheritance chains.
▸ Mixin Classes
# Mixins add capabilities without being standalone classes
class SerializeMixin:
'''Adds JSON/dict serialization to any class'''
def to_dict(self):
return {k: v for k, v in self.__dict__.items()
if not [Link]('_')}
def to_json(self):
import json
return [Link](self.to_dict(), indent=2)
class LogMixin:
'''Adds logging to any class'''
def log(self, msg):
import datetime
ts = [Link]().strftime('%H:%M:%S')
print(f'[{ts}] {type(self).__name__}: {msg}')
class ValidateMixin:
'''Adds validation helpers'''
def validate_not_empty(self, value, field):
if not value or not str(value).strip():
raise ValueError(f'{field} cannot be empty')
def validate_positive(self, value, field):
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f'{field} must be a positive number')
# Use mixins to compose a full-featured class
class Product(SerializeMixin, LogMixin, ValidateMixin):
def __init__(self, name, price, qty):
self.validate_not_empty(name, 'Product name')
self.validate_positive(price, 'Price')
self.validate_positive(qty, 'Quantity')
[Link] = name
[Link] = price
[Link] = qty
[Link](f'Created product: {name}')
def sell(self, units):
self.validate_positive(units, 'Units')
if units > [Link]:
raise ValueError('Insufficient stock')
[Link] -= units
[Link](f'Sold {units} units. Remaining: {[Link]}')
return units * [Link]
p = Product('Laptop', 75000, 10)
revenue = [Link](3)
print(p.to_json())
print(f'Revenue: {revenue}')
Section 4: Polymorphism — All Forms
4.1 Method Overriding (Runtime Polymorphism)
▸ Method Overriding
# Classic polymorphism — same interface, different implementations
class Shape:
def __init__(self, color='white'):
[Link] = color
def area(self):
raise NotImplementedError('Subclass must implement area()')
def perimeter(self):
raise NotImplementedError('Subclass must implement perimeter()')
def describe(self):
# Uses [Link]() — calls the OVERRIDDEN version at runtime!
return (f'{type(self).__name__}: color={[Link]}, '
f'area={[Link]():.2f}, perimeter={[Link]():.2f}')
class Circle(Shape):
def __init__(self, radius, color='white'):
super().__init__(color)
[Link] = radius
def area(self):
import math
return [Link] * [Link] ** 2
def perimeter(self):
import math
return 2 * [Link] * [Link]
class Rectangle(Shape):
def __init__(self, w, h, color='white'):
super().__init__(color)
self.w, self.h = w, h
def area(self): return self.w * self.h
def perimeter(self): return 2 * (self.w + self.h)
class Triangle(Shape):
def __init__(self, a, b, c, color='white'):
super().__init__(color)
self.a, self.b, self.c = a, b, c
def perimeter(self): return self.a + self.b + self.c
def area(self):
s = [Link]() / 2 # Heron's formula
return (s*(s-self.a)*(s-self.b)*(s-self.c)) ** 0.5
# POLYMORPHISM — we call the SAME method on different types
shapes = [Circle(7,'red'), Rectangle(4,6,'blue'), Triangle(3,4,5,'green')]
for shape in shapes:
print([Link]())
# Calculate total area — works regardless of shape type
total_area = sum([Link]() for shape in shapes)
print(f'Total area: {total_area:.2f}')
# sorted() works because we can compare shapes
[Link](key=lambda s: [Link]())
print([type(s).__name__ for s in shapes])
4.2 Operator Overloading (Dunder Methods)
▸ Operator Overloading — Fraction Class
class Fraction:
'''Represents a/b with full arithmetic support'''
def __init__(self, num, den=1):
if den == 0:
raise ZeroDivisionError('Denominator cannot be zero')
# Always simplify
from math import gcd
g = gcd(abs(num), abs(den))
sign = -1 if den < 0 else 1
[Link] = sign * num // g
[Link] = sign * den // g
def __str__(self): return f'{[Link]}/{[Link]}'
def __repr__(self): return f'Fraction({[Link]}, {[Link]})'
def __add__(self, other):
if isinstance(other, int): other = Fraction(other)
return Fraction([Link]*[Link] + [Link]*[Link],
[Link] * [Link])
def __sub__(self, other):
if isinstance(other, int): other = Fraction(other)
return Fraction([Link]*[Link] - [Link]*[Link],
[Link] * [Link])
def __mul__(self, other):
if isinstance(other, int): other = Fraction(other)
return Fraction([Link]*[Link], [Link]*[Link])
def __truediv__(self, other):
if isinstance(other, int): other = Fraction(other)
return Fraction([Link]*[Link], [Link]*[Link])
def __eq__(self, other):
return [Link] == [Link] and [Link] == [Link]
def __lt__(self, other):
return [Link] * [Link] < [Link] * [Link]
def __float__(self): return [Link] / [Link]
def __int__(self): return [Link] // [Link]
def __abs__(self): return Fraction(abs([Link]), [Link])
def __neg__(self): return Fraction(-[Link], [Link])
def __bool__(self): return [Link] != 0
a = Fraction(1, 2) # 1/2
b = Fraction(1, 3) # 1/3
print(a + b) # 5/6
print(a - b) # 1/6
print(a * b) # 1/6
print(a / b) # 3/2
print(a > b) # True
print(float(a)) # 0.5
fracs = [Fraction(3,4), Fraction(1,2), Fraction(2,3)]
print(sorted(fracs)) # [1/2, 2/3, 3/4]
Section 5: Abstraction — ABC and Interfaces
▸ Abstract Classes — Payment Gateway
from abc import ABC, abstractmethod
# Abstract class: defines WHAT to do, not HOW
class PaymentGateway(ABC):
'''Abstract interface for all payment methods'''
@abstractmethod
def connect(self):
pass
@abstractmethod
def charge(self, amount, currency='INR'):
pass
@abstractmethod
def refund(self, transaction_id):
pass
# Concrete method in abstract class — shared by all
def log_transaction(self, txn_type, amount):
import datetime
print(f'[{[Link]():%H:%M:%S}] {txn_type}: {amount}')
@property
@abstractmethod
def gateway_name(self):
pass
# Can't instantiate abstract class
# p = PaymentGateway() → TypeError: Can't instantiate abstract class
class RazorpayGateway(PaymentGateway):
def __init__(self, api_key):
self.api_key = api_key
self._connected = False
@property
def gateway_name(self): return 'Razorpay'
def connect(self):
self._connected = True
return f'{self.gateway_name} connected'
def charge(self, amount, currency='INR'):
if not self._connected:
raise RuntimeError('Not connected')
self.log_transaction('CHARGE', amount)
return f'TXN_{id(self)}_{amount}'
def refund(self, transaction_id):
self.log_transaction('REFUND', transaction_id)
return True
class StripeGateway(PaymentGateway):
def __init__(self, secret_key):
self.secret_key = secret_key
@property
def gateway_name(self): return 'Stripe'
def connect(self):
return f'{self.gateway_name} connected'
def charge(self, amount, currency='USD'):
self.log_transaction('CHARGE', f'{amount} {currency}')
return f'pi_{id(self)}'
def refund(self, transaction_id):
return {'status': 'success', 'id': transaction_id}
# Same code works with ANY payment gateway
def process_order(gateway: PaymentGateway, total: float):
[Link]()
txn_id = [Link](total)
print(f'Paid via {gateway.gateway_name}: {txn_id}')
return txn_id
process_order(RazorpayGateway('rzp_key'), 1500)
process_order(StripeGateway('sk_secret'), 20)
Section 6: Dunder Methods — Complete Reference
Definition: Dunder methods (double underscore) let you define how your objects interact with Python's built-in
operations. They're called automatically by Python — you never call obj.__add__(other) directly; Python
calls it when you write obj + other.
6.1 String Representation
▸ String Representation
class Color:
def __init__(self, r, g, b):
self.r, self.g, self.b = r, g, b
def __str__(self):
'''Human-readable: used by print(), str(), f-strings'''
return f'rgb({self.r}, {self.g}, {self.b})'
def __repr__(self):
'''Developer representation: used in console, lists, repr()'''
return f'Color({self.r}, {self.g}, {self.b})'
def __format__(self, spec):
'''Custom f-string formatting'''
if spec == 'hex':
return f'#{self.r:02X}{self.g:02X}{self.b:02X}'
if spec == 'css':
return f'rgb({self.r},{self.g},{self.b})'
return str(self)
red = Color(255, 0, 0)
blue = Color(0, 0, 255)
print(red) # rgb(255, 0, 0) — __str__
print(repr(red)) # Color(255, 0, 0) — __repr__
print(f'{red:hex}') # #FF0000 — __format__
print(f'{blue:css}') # rgb(0,0,255)
colors = [red, blue]
print(colors) # [Color(255, 0, 0), Color(0, 0, 255)] — uses
__repr__
6.2 Container Protocol
▸ Container Protocol
class Playlist:
'''Custom container behaving like a list of songs'''
def __init__(self, name):
[Link] = name
self._songs = []
def __len__(self): # len(playlist)
return len(self._songs)
def __getitem__(self, index): # playlist[0], playlist[1:3]
return self._songs[index]
def __setitem__(self, index, song): # playlist[0] = 'song'
self._songs[index] = song
def __delitem__(self, index): # del playlist[0]
del self._songs[index]
def __contains__(self, song): # 'song' in playlist
return song in self._songs
def __iter__(self): # for song in playlist:
return iter(self._songs)
def __reversed__(self): # reversed(playlist)
return reversed(self._songs)
def __bool__(self): # if playlist:
return len(self._songs) > 0
def append(self, song):
self._songs.append(song)
def __str__(self):
return f'Playlist "{[Link]}": {self._songs}'
pl = Playlist('Chill Vibes')
[Link]('Song A')
[Link]('Song B')
[Link]('Song C')
print(len(pl)) # 3
print(pl[0]) # Song A
print(pl[1:3]) # ['Song B', 'Song C']
print('Song B' in pl) # True
for song in pl: print(song) # Song A, Song B, Song C
print(bool(pl)) # True
empty = Playlist('Empty')
print(bool(empty)) # False
6.3 Context Manager Protocol
▸ Context Manager Protocol
# __enter__ and __exit__ enable 'with' statement
class ManagedFile:
def __init__(self, filename, mode='r'):
[Link] = filename
[Link] = mode
self._file = None
def __enter__(self):
print(f'Opening {[Link]}')
self._file = open([Link], [Link])
return self._file # this becomes the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
print(f'Closing {[Link]}')
if self._file:
self._file.close()
# Return False/None → exception propagates
# Return True → exception is suppressed
return False
# Usage
# with ManagedFile('[Link]', 'w') as f:
# [Link]('Hello!')
# Automatically closes even if exception occurs
# Timer context manager
import time
class Timer:
def __init__(self, name=''):
[Link] = name
def __enter__(self):
[Link] = time.perf_counter()
return self
def __exit__(self, *args):
[Link] = time.perf_counter() - [Link]
label = f'[{[Link]}] ' if [Link] else ''
print(f'{label}Elapsed: {[Link]:.4f}s')
return False
with Timer('sum computation'):
result = sum(range(1_000_000))
print(f'Sum: {result}')
Section 7: dataclasses — Modern Python OOP
Definition: The @dataclass decorator (Python 3.7+) automatically generates __init__, __repr__, and
__eq__ for you. This eliminates the boilerplate of writing constructors for data-holding classes.
▸ dataclasses
from dataclasses import dataclass, field, asdict, astuple
@dataclass
class Point:
x: float
y: float
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
print(p1) # Point(x=3.0, y=4.0) — __repr__ auto-generated
print(p1 == p2) # True — __eq__ auto-generated
@dataclass
class Student:
name: str
age: int
gpa: float = 0.0 # default value
courses: list = field(default_factory=list) # mutable default!
def __post_init__(self):
# Called after auto-generated __init__
if [Link] < 0:
raise ValueError('Age cannot be negative')
[Link] = [Link]() # capitalize name
s = Student('alice', 20, 3.8)
print(s) # Student(name='Alice', age=20, gpa=3.8, courses=[])
[Link]('Python')
@dataclass(frozen=True) # immutable — can be used as dict key
class ImmutablePoint:
x: float
y: float
def distance(self):
return (self.x**2 + self.y**2) ** 0.5
@dataclass(order=True) # generates __lt__, __le__, __gt__, __ge__
class Player:
score: int = field(compare=True) # sort by score
name: str = field(compare=False) # don't compare by name
players = [Player(85, 'Alice'), Player(92, 'Bob'), Player(78, 'Carol')]
[Link](reverse=True)
print(players) # sorted by score descending
# Convert to dict / tuple
s_dict = asdict(s)
print(s_dict) # {'name': 'Alice', 'age': 20, 'gpa': 3.8, 'courses':
['Python']}
Section 8: __slots__ — Memory Optimization
▸ __slots__
# Normal class: each instance has a __dict__ (wastes memory for many
instances)
class PointNormal:
def __init__(self, x, y):
self.x, self.y = x, y
# __slots__ class: no __dict__, uses slots array instead (faster + less
memory)
class PointSlots:
__slots__ = ['x', 'y'] # ONLY these attributes allowed
def __init__(self, x, y):
self.x, self.y = x, y
import sys
pn = PointNormal(3, 4)
ps = PointSlots(3, 4)
print([Link](pn)) # ~48 bytes + __dict__ (~200 bytes)
print([Link](ps)) # ~56 bytes — no __dict__
# Attempting to add attribute fails with __slots__
pn.z = 5 # works for normal class
# ps.z = 5 # AttributeError: 'PointSlots' object has no attribute 'z'
# When to use __slots__:
# - Creating millions of instances (e.g., simulation, game entities)
# - Objects where attributes are always the same fixed set
# Memory comparison with 1 million objects
import tracemalloc
[Link]()
pts_normal = [PointNormal(i, i) for i in range(100000)]
mem1 = tracemalloc.get_traced_memory()[1]
tracemalloc.reset_peak()
pts_slots = [PointSlots(i, i) for i in range(100000)]
mem2 = tracemalloc.get_traced_memory()[1]
[Link]()
print(f'Normal: {mem1/1e6:.1f}MB, Slots: {mem2/1e6:.1f}MB')
Section 9: @property, @classmethod, @staticmethod — Advanced
▸ Advanced Properties and Factory Methods
class Temperature:
'''Full temperature class with multiple unit properties'''
ABSOLUTE_ZERO_C = -273.15
def __init__(self, celsius):
[Link] = celsius # uses the setter below
@property
def celsius(self):
return self._celsius
@[Link]
def celsius(self, value):
if value < Temperature.ABSOLUTE_ZERO_C:
raise ValueError(f'Below absolute zero! Min:
{Temperature.ABSOLUTE_ZERO_C}')
self._celsius = value
@[Link]
def celsius(self):
print('Deleting celsius')
del self._celsius
# Computed properties — no separate variable needed
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@[Link]
def fahrenheit(self, value):
[Link] = (value - 32) * 5/9 # goes through celsius setter!
@property
def kelvin(self):
return self._celsius - Temperature.ABSOLUTE_ZERO_C
@[Link]
def kelvin(self, value):
[Link] = value + Temperature.ABSOLUTE_ZERO_C
# Factory class methods
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5/9)
@classmethod
def from_kelvin(cls, k):
return cls(k + Temperature.ABSOLUTE_ZERO_C)
,
@classmethod
def absolute_zero(cls):
return cls(cls.ABSOLUTE_ZERO_C)
def __str__(self):
return f'{self._celsius:.2f}°C / {[Link]:.2f}°F /
{[Link]:.2f}K'
t1 = Temperature(100)
print(t1) # 100.00°C / 212.00°F / 373.15K
[Link] = 32 # uses [Link] which calls
[Link]
print([Link]) # 0.0
t2 = Temperature.from_fahrenheit(98.6) # body temp
print([Link]) # 37.0
t3 = Temperature.absolute_zero()
print(t3) # -273.15°C / -459.67°F / 0.00K
PART B
PYTHON STANDARD LIBRARY — ALL IMPORTANT MODULES
Overview: Python Standard Library
Definition: The Python Standard Library (PSL) is a collection of modules and packages included with every
Python installation — you don't need to install them separately. It covers file I/O, math, dates, networking,
threading, compression, databases, email, HTML parsing, testing, and much more.
📦 Module Categories
Text & Strings: string, re, textwrap, unicodedata, difflib
Numbers & Math: math, cmath, decimal, fractions, statistics, random
Data Structures: collections, heapq, bisect, array, queue
File & I/O: os, io, pathlib, shutil, tempfile, glob, fnmatch
Date & Time: datetime, time, calendar, zoneinfo
Data Formats: json, csv, configparser, xml, html, base64
Compression: zipfile, tarfile, gzip, bz2, lzma, zlib
Iteration: itertools, functools, operator
OS & System: sys, os, platform, subprocess, signal, ctypes
Networking: socket, urllib, http, email, smtplib, ftplib
Concurrency: threading, multiprocessing, asyncio, [Link]
Testing & Debug: unittest, doctest, logging, pdb, traceback, inspect
Type Hints: typing, types, abc
Module 1: os — Operating System Interface
Definition: The os module provides portable functions for OS operations: paths, directories, files, processes,
and environment variables. It abstracts differences between Windows (backslash, drive letters) and Unix
(forward slash, no drives).
▸ os Module — Complete
import os
# ── CURRENT LOCATION ──
print([Link]()) # /home/user/projects
[Link]('/tmp') # change working directory
print([Link]()) # /tmp
# ── DIRECTORY OPERATIONS ──
[Link]('test_dir') # create single directory
[Link]('a/b/c/d') # create full nested path
[Link]('a/b/c/d', exist_ok=True) # no error if exists
[Link]('test_dir') # remove empty directory
[Link]('a/b/c/d') # remove nested empty dirs
# ── FILE OPERATIONS ──
[Link]('[Link]', '[Link]') # rename file
[Link]('old/[Link]', 'new/path') # rename with intermediate dirs
[Link]('[Link]', '[Link]') # rename, overwriting dst
[Link]('[Link]') # delete file
[Link]('[Link]') # same as remove
# ── LISTING & WALKING ──
files = [Link]('.') # returns list of all entries
entries = [Link]('.') # iterator of DirEntry objects (faster!)
for entry in [Link]('.'):
if entry.is_file():
print(f'File: {[Link]}, size: {[Link]().st_size}B')
elif entry.is_dir():
print(f'Dir: {[Link]}')
# [Link] — recursively traverse directory tree
for root, dirs, files in [Link]('/tmp'):
level = [Link]('/tmp', '').count([Link])
indent = ' ' * level
print(f'{indent}{[Link](root)}/')
for f in files:
print(f'{indent} {f}')
# ── PATH OPERATIONS ──
p = '/home/user/projects/app/[Link]'
print([Link](p)) # [Link]
print([Link](p)) # /home/user/projects/app
print([Link](p)) # ('/home/.../main', '.py')
print([Link](p)) # ('/home/.../app', '[Link]')
print([Link]('[Link]')) # full absolute path
print([Link]('~/docs')) # expands ~
# Joining paths (handles OS differences automatically)
full = [Link]('/home', 'user', 'docs', '[Link]')
print(full) # /home/user/docs/[Link] (Linux)
# \home\user\docs\[Link] (Windows)
# Checks
print([Link]('/etc/hosts')) # True
print([Link]('/etc/hosts')) # True
print([Link]('/etc')) # True
print([Link]('/etc/hosts')) # True
print([Link]('/etc/hosts')) # size in bytes
# ── ENVIRONMENT VARIABLES ──
print([Link]('HOME')) # /home/user
print([Link]('PATH', '')) # PATH variable
[Link]['MY_APP_KEY'] = 'secret' # set for current process
[Link]('MY_APP_KEY') # unset
# ── PROCESS OPERATIONS ──
print([Link]()) # current process ID
print([Link]()) # parent process ID
[Link]('echo Hello') # run shell command
# ── FILE STATS ──
stat = [Link]('[Link]')
print(stat.st_size) # file size in bytes
print(stat.st_mtime) # modification time (Unix timestamp)
Module 2: sys — Python Interpreter Interface
▸ sys Module — Complete
import sys
# ── VERSION ──
print([Link]) # '3.11.0 (main, Oct 24 2022, ...)'
print(sys.version_info) # sys.version_info(major=3, minor=11, micro=0, ...)
if sys.version_info < (3, 8):
print('Please upgrade to Python 3.8+')
# ── COMMAND-LINE ARGUMENTS ──
# Run: python [Link] hello world 42
print([Link]) # ['[Link]', 'hello', 'world', '42']
print([Link][0]) # '[Link]' (the script itself)
args = [Link][1:] # ['hello', 'world', '42']
# ── MODULE SEARCH PATH ──
print([Link]) # list of dirs Python searches
[Link](0, '/my/custom/lib') # add dir to FRONT of search path
[Link]('/another/path') # add to END
# ── STREAMS ──
[Link]('Hello\n') # same as print('Hello')
[Link]('Error!\n') # write to standard error
# [Link]() # read from standard input
# Redirect stdout
import io
old_stdout = [Link]
[Link] = [Link]() # capture output
print('captured!')
output = [Link]()
[Link] = old_stdout # restore
print(f'Captured: {output!r}') # Captured: 'captured!\n'
# ── MEMORY ──
lst = [1, 2, 3, 4, 5]
print([Link](lst)) # ~120 bytes
print([Link](42)) # 28 bytes
print([Link]('hello')) # 54 bytes
# ── RECURSION LIMIT ──
print([Link]()) # 1000
[Link](5000) # increase for deep recursion
# ── EXIT ──
# [Link](0) # exit with code 0 (success)
# [Link](1) # exit with code 1 (error)
# [Link]('Error message') # prints message to stderr, exits 1
# ── PLATFORM & IMPLEMENTATION ──
print([Link]) # 'linux', 'win32', 'darwin'
print([Link]) # path to Python interpreter
print([Link]) # Python install prefix
# ── MODULES ──
print('json' in [Link]) # True if already imported
print(list([Link]())[:5]) # first 5 loaded modules
Module 3: math — Mathematical Functions
▸ math Module — Complete
import math
# ── CONSTANTS ──
print([Link]) # 3.141592653589793
print(math.e) # 2.718281828459045 (Euler's number)
print([Link]) # 6.283185... = 2*pi
print([Link]) # infinity
print([Link]) # Not a Number
# ── ROUNDING ──
print([Link](4.7)) # 4 (rounds DOWN always)
print([Link](4.2)) # 5 (rounds UP always)
print([Link](4.9)) # 4 (truncates decimal)
print(round(4.5)) # 4 (banker's rounding — built-in)
print(round(5.5)) # 6 (banker's rounding)
print([Link](-4.3)) # -5 (floor goes toward -inf!)
print([Link](-4.7)) # -4 (ceil goes toward +inf!)
# ── POWERS & LOGARITHMS ──
print([Link](144)) # 12.0
print([Link](2, 10)) # 1024.0 (always float)
print(2 ** 10) # 1024 (int ** int = int)
print([Link](1)) # 2.718... = e^1
print([Link](2)) # 7.389... = e^2
print([Link](math.e)) # 1.0 (natural log, base e)
print([Link](100, 10)) # 2.0 (log base 10 of 100)
print(math.log2(1024)) # 10.0 (log base 2)
print(math.log10(1000)) # 3.0 (log base 10)
# ── TRIGONOMETRY ── (all in RADIANS)
print([Link]([Link]/2)) # 1.0
print([Link](0)) # 1.0
print([Link]([Link]/4)) # 1.0
print([Link]([Link])) # 180.0 (rad → deg)
print([Link](180)) # 3.14... (deg → rad)
print([Link](1.0)) # 1.57... = pi/2
print(math.atan2(1, 1)) # 0.785... = pi/4 (y/x)
# ── NUMBER THEORY ──
print([Link](10)) # 3628800
print([Link](48, 36)) # 12
print([Link](4, 6)) # 12 (Python 3.9+)
print([Link](10, 3)) # 120 10C3
print([Link](5, 2)) # 20 5P2
# ── HYPERBOLIC ──
print([Link](3, 4)) # 5.0 sqrt(3^2 + 4^2)
print([Link](1,2,3)) # 3.74... (N-dimensional, 3.8+)
# ── CHECKS ──
print([Link](1.5)) # True
print([Link]([Link])) # True
print([Link](float('nan'))) # True
print([Link](0.1+0.2, 0.3, rel_tol=1e-9)) # True!
Module 4: random — Random Number Generation
▸ random Module — Complete
import random
# ── SEEDING (reproducible randomness) ──
[Link](42) # set seed → same sequence every run
print([Link]()) # 0.6394267984578837 (same every time with seed 42)
# ── FLOATS ──
print([Link]()) # float in [0.0, 1.0)
print([Link](1.0, 5.0)) # float in [1.0, 5.0]
print([Link](0, 1)) # Gaussian distribution (mean=0, std=1)
print([Link](50, 10)) # normal dist (mean=50, std=10)
# ── INTEGERS ──
print([Link](1, 6)) # int in [1, 6] inclusive (dice roll)
print([Link](0, 100, 5)) # random multiple of 5 in [0, 100)
print([Link](10)) # random in range(10) = 0..9
# ── SEQUENCES ──
deck = list(range(1, 53))
print([Link](deck)) # pick one random element
colors = ['red', 'green', 'blue', 'yellow']
print([Link](colors, k=3)) # 3 picks WITH replacement
print([Link](colors, weights=[4,2,1,1], k=5)) # weighted
print([Link](deck, 5)) # 5 unique picks WITHOUT replacement
hand = list(range(1, 53))
[Link](hand) # shuffle IN PLACE
print(hand[:5]) # first 5 cards after shuffle
# ── PRACTICAL EXAMPLES ──
# Random password
import string
alphabet = string.ascii_letters + [Link] + '!@#$%'
password = ''.join([Link](alphabet, k=16))
print(f'Password: {password}')
# Simulate dice rolling
def roll_dice(n=2, sides=6):
return [[Link](1, sides) for _ in range(n)]
rolls = roll_dice()
print(f'Rolled: {rolls}, Total: {sum(rolls)}')
# Random test data
names = ['Alice','Bob','Carol','Dave','Eve']
sample_users = [Link](names, 3)
print(f'Test users: {sample_users}')
Module 5: datetime — Date and Time Handling
▸ datetime Module — Complete
from datetime import datetime, date, time, timedelta, timezone
# ── CREATING DATES AND TIMES ──
now = [Link]() # current LOCAL datetime
utc = [Link]() # current UTC datetime
today = [Link]() # current local date only
dt = datetime(2025, 12, 25, 10, 30, 0) # Christmas 10:30 AM
d = date(2025, 1, 26) # Republic Day
t = time(9, 15, 0) # 9:15 AM
# ── ACCESSING COMPONENTS ──
print([Link], [Link], [Link]) # 2025 12 25
print([Link], [Link], [Link]) # 10 30 0
print([Link]) # 0
print([Link]()) # 0=Mon,1=Tue,...6=Sun → 3=Thu
print([Link]()) # 1=Mon,...7=Sun
print([Link]()) # (year, week_number, weekday)
print([Link]('%A')) # e.g., 'Wednesday'
# ── FORMATTING: strftime (datetime → string) ──
print([Link]('%Y-%m-%d %H:%M:%S')) # 2025-03-11 14:30:45
print([Link]('%d/%m/%Y')) # 11/03/2025
print([Link]('%B %d, %Y')) # March 11, 2025
print([Link]('%I:%M %p')) # 02:30 PM
print([Link]('%A, %d %b %Y')) # Wednesday, 11 Mar 2025
print([Link]()) # 2025-03-11T14:30:45.123456
# strftime format codes
# %Y = 4-digit year, %m = month (01-12), %d = day (01-31)
# %H = 24h hour, %I = 12h hour, %M = minute, %S = second
# %A = weekday name, %a = short name, %B = month name, %b = short
# %p = AM/PM, %j = day of year, %W = week number, %Z = timezone
# ── PARSING: strptime (string → datetime) ──
s1 = '2025-07-15 09:00:00'
dt1 = [Link](s1, '%Y-%m-%d %H:%M:%S')
s2 = '15 August 2025'
dt2 = [Link](s2, '%d %B %Y')
# ── TIMEDELTA — arithmetic ──
one_week = timedelta(weeks=1)
two_days = timedelta(days=2)
two_hours = timedelta(hours=2)
mixed = timedelta(days=3, hours=6, minutes=30)
# Add/subtract
next_week = today + one_week
yesterday = today - timedelta(days=1)
deadline = now + timedelta(days=30)
# Difference between dates
bday = date(2000, 1, 1)
age_days = (today - bday).days
print(f'Age in days: {age_days}, years: {age_days//365}')
# Duration between two datetimes
start = datetime(2025, 1, 1, 9, 0)
end = datetime(2025, 1, 1, 17, 30)
duration = end - start
print(f'Duration: {duration}') # 8:30:00
print(f'Total seconds: {duration.total_seconds()}') # 30600.0
# ── TIMEZONE-AWARE DATETIME ──
utc_tz = [Link]
now_utc = [Link](utc_tz) # timezone-aware now
# Convert between timezones using zoneinfo (Python 3.9+)
from zoneinfo import ZoneInfo
ist = ZoneInfo('Asia/Kolkata')
now_ist = [Link](ist)
print(now_ist.strftime('%Z %z')) # IST +0530
Module 6: collections — Advanced Data Structures
▸ collections — All Classes
from collections import (Counter, defaultdict, deque,
OrderedDict, namedtuple, ChainMap, UserDict)
# ══ Counter ══
text = 'to be or not to be that is the question'
words = [Link]()
wc = Counter(words)
print(wc.most_common(3)) # [('be',2),('to',2),('or',1)]
print(wc['be']) # 2
print(wc['missing']) # 0 (no KeyError!)
# Counter arithmetic
a = Counter({'x':5, 'y':2, 'z':8})
b = Counter({'x':1, 'y':3})
print(a + b) # Counter({'z':8,'x':6,'y':5})
print(a - b) # Counter({'z':8,'x':4})
print(a & b) # Counter({'x':1,'y':2}) — minimum
print(a | b) # Counter({'z':8,'x':5,'y':3}) — maximum
# Count characters
char_freq = Counter('abracadabra')
print(char_freq) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
# ══ defaultdict ══
# Groups words by first letter
from_alpha = defaultdict(list)
for w in words:
from_alpha[w[0]].append(w)
print(dict(from_alpha))
# Nested defaultdict
graph = defaultdict(set) # adjacency list
edges = [('A','B'),('A','C'),('B','C'),('C','D')]
for u, v in edges:
graph[u].add(v)
graph[v].add(u)
print(dict(graph))
# ══ deque ══
# Efficient double-ended queue: O(1) append/pop from both ends
dq = deque([1,2,3,4,5])
[Link](0) # [0,1,2,3,4,5]
[Link](6) # [0,1,2,3,4,5,6]
[Link]() # removes 0
[Link]() # removes 6
[Link](2) # [4,5,1,2,3]
[Link](-2) # [1,2,3,4,5]
# Sliding window — maxlen auto-discards old items
window = deque(maxlen=5)
for i in range(10):
[Link](i)
if len(window) == [Link]:
print(f'Window: {list(window)}, avg: {sum(window)/5:.1f}')
# ══ namedtuple ══
Point = namedtuple('Point', ['x', 'y', 'z'])
p = Point(1, 2, 3)
print(p.x, p.y, p.z) # 1 2 3
print(p[0]) # 1 (indexing works)
print(p._asdict()) # {'x':1,'y':2,'z':3}
p2 = p._replace(z=99) # create modified copy
x, y, z = p # unpacking works
Employee = namedtuple('Employee', 'name dept salary')
alice = Employee('Alice', 'Eng', 90000)
print(alice)
# ══ OrderedDict ══
od = OrderedDict()
od['first'] = 1
od['second'] = 2
od['third'] = 3
od.move_to_end('first') # move to end
od.move_to_end('second', last=False) # move to front
print(list([Link]())) # ['second', 'third', 'first']
# ══ ChainMap ══
defaults = {'theme':'light','lang':'en','debug':False}
env = {'debug': True}
args = {'theme': 'dark'}
# Priority: args > env > defaults
config = ChainMap(args, env, defaults)
print(config['theme']) # dark (from args)
print(config['debug']) # True (from env)
print(config['lang']) # en (from defaults)
config['new_key'] = 'val' # writes to FIRST map (args)
Module 7: itertools — Iterator Tools
▸ itertools — Complete
import itertools
# ══ INFINITE ITERATORS ══
# count(start=0, step=1) — counts forever
for n in [Link]([Link](10, 3), 6):
print(n, end=' ') # 10 13 16 19 22 25
print()
# cycle(iterable) — loops forever
days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
for d in [Link]([Link](days), 10):
print(d, end=' ') # Mon Tue ... Sun Mon Tue Mon
print()
# repeat(object, times=None)
print(list([Link]('X', 5))) # ['X','X','X','X','X']
# ══ TERMINATING ITERATORS ══
# chain — flatten multiple iterables
c = list([Link]('ABC', [1,2,3], (True,False)))
print(c) # ['A','B','C',1,2,3,True,False]
nested = [[1,2],[3,4],[5,6]]
flat = list([Link].from_iterable(nested))
print(flat) # [1,2,3,4,5,6]
# compress — filter by selector
data = ['A','B','C','D','E']
selector = [1, 0, 1, 0, 1]
print(list([Link](data, selector))) # ['A','C','E']
# dropwhile / takewhile
nums = [1,2,3,4,5,4,3,2,1]
print(list([Link](lambda x: x<4, nums))) # [4,5,4,3,2,1]
print(list([Link](lambda x: x<4, nums))) # [1,2,3]
# filterfalse — opposite of filter
print(list([Link](lambda x: x%2, range(10))))
# [0, 2, 4, 6, 8]
# islice(iter, stop) or islice(iter, start, stop, step)
print(list([Link](range(100), 5, 20, 3))) # [5,8,11,14,17]
# starmap — map with unpacking
pairs = [(2,3),(3,3),(4,3)]
print(list([Link](pow, pairs))) # [8,27,64]
# tee — split one iterator into n independent ones
iter1, iter2 = [Link]([1,2,3,4,5], 2)
print(list(iter1)) # [1,2,3,4,5]
print(list(iter2)) # [1,2,3,4,5]
# zip_longest — zip with fillvalue
a, b = [1,2,3], ['x','y']
print(list(itertools.zip_longest(a, b, fillvalue='-')))
# [(1,'x'),(2,'y'),(3,'-')]
# ══ COMBINATORIC ITERATORS ══
# product — cartesian product
print(list([Link]('AB', repeat=2)))
# [('A','A'),('A','B'),('B','A'),('B','B')]
# permutations — all orderings
print(list([Link]('ABC', 2)))
# 6 pairs: ('A','B'),('A','C'),('B','A'),('B','C'),('C','A'),('C','B')
# combinations — no repeats, order doesn't matter
print(list([Link]('ABCD', 2)))
# ('A','B'),('A','C'),('A','D'),('B','C'),('B','D'),('C','D')
# combinations_with_replacement
print(list(itertools.combinations_with_replacement('AB', 3)))
# [('A','A','A'),('A','A','B'),('A','B','B'),('B','B','B')]
# groupby — group consecutive elements by key
data = sorted(['cat','car','cow','dog','door','duck'], key=lambda w:w[0])
for key, group in [Link](data, key=lambda w:w[0]):
print(f'{key}: {list(group)}')
# c: ['cat','car','cow'] d: ['dog','door','duck']
# accumulate
print(list([Link]([1,2,3,4,5]))) # running sum
[1,3,6,10,15]
print(list([Link]([1,2,3,4,5], max))) # running max
[1,2,3,4,5]
print(list([Link]([2,2,2,2,2], lambda a,b:a*b))) #
[2,4,8,16,32]
Module 8: functools — Higher-Order Functions
▸ functools — Complete
import functools
# ══ reduce ══
# reduce(f, [a,b,c,d]) = f(f(f(a,b),c),d) — fold left
product = [Link](lambda a,b: a*b, range(1,6))
print(product) # 120 = 5!
# With initial value
s = [Link](lambda acc,x: acc+str(x), range(5), '')
print(s) # '01234'
# ══ partial ══
# Fix some arguments of a function
def power(base, exponent):
return base ** exponent
square = [Link](power, exponent=2)
cube = [Link](power, exponent=3)
print([square(x) for x in range(1,6)]) # [1,4,9,16,25]
print([cube(x) for x in range(1,6)]) # [1,8,27,64,125]
# Practical: pre-fill API arguments
import [Link]
encode_utf8 = [Link]([Link], encoding='utf-8')
# ══ lru_cache ══
# Memoize: cache up to `maxsize` recent results
@functools.lru_cache(maxsize=256)
def expensive_calc(n):
import time
[Link](0.001) # simulate slow computation
return n * n
# First call: computed
print(expensive_calc(10)) # 100 (slow)
# Second call: from cache
print(expensive_calc(10)) # 100 (instant)
print(expensive_calc.cache_info()) # hits, misses, maxsize, currsize
expensive_calc.cache_clear() # clear cache
# Fibonacci with cache — O(n) instead of O(2^n)
@functools.lru_cache(maxsize=None)
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
print(fib(100)) # 354224848179261915075 — instant!
# ══ cache (Python 3.9+) ══
from functools import cache
@cache
def fact(n):
return 1 if n <= 1 else n * fact(n-1)
# ══ total_ordering ══
# Define __eq__ and ONE of __lt__/__le__/__gt__/__ge__
# Decorator generates the rest automatically
@functools.total_ordering
class Version:
def __init__(self, major, minor, patch):
self.v = (major, minor, patch)
def __eq__(self, other): return self.v == other.v
def __lt__(self, other): return self.v < other.v
def __str__(self): return '.'.join(map(str, self.v))
v1 = Version(1,2,3)
v2 = Version(2,0,0)
print(v1 < v2) # True
print(v1 > v2) # False (auto-generated!)
print(v1 <= v2) # True (auto-generated!)
versions = [Version(1,10,0), Version(1,9,5), Version(2,0,0)]
print(sorted(versions)) # [1.9.5, 1.10.0, 2.0.0]
# ══ wraps ══
# Preserve original function metadata in decorators
def my_decorator(func):
@[Link](func) # preserves __name__, __doc__, etc.
def wrapper(*args, **kwargs):
print('Before')
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
'''Adds two numbers'''
return a + b
print(add.__name__) # 'add' (NOT 'wrapper'!)
print(add.__doc__) # 'Adds two numbers'
Module 9: re — Regular Expressions
▸ re Module — Complete
import re
# ── BASIC FUNCTIONS ──
text = 'Hello World 123, Python 3.11!'
# [Link] — find FIRST match anywhere
m = [Link](r'\d+', text)
print([Link]()) # '123'
print([Link](), [Link]()) # 12 15
# [Link] — match at START only
m = [Link](r'Hello', text) # matches
m = [Link](r'World', text) # None (not at start!)
# [Link] — entire string must match
m = [Link](r'\d{4}-\d{2}-\d{2}', '2025-03-11') # matches
# [Link] — return ALL matches as list
nums = [Link](r'\d+', text)
print(nums) # ['123', '3', '11']
words = [Link](r'[A-Z][a-z]+', text)
print(words) # ['Hello', 'World', 'Python']
# [Link] — return iterator of match objects
for m in [Link](r'\d+', text):
print(f'{[Link]()} at {[Link]()}-{[Link]()}')
# [Link] — replace
clean = [Link](r'\d+', 'NUM', text)
print(clean) # 'Hello World NUM, Python [Link]!'
# [Link] with function
doubled = [Link](r'\d+', lambda m: str(int([Link]())*2), text)
print(doubled) # '...246, Python 6.22!'
# [Link] — split on pattern
parts = [Link](r'[\s,!]+', text)
print(parts) # ['Hello','World','123','Python','3.11','']
# ── GROUPS ──
# () creates a capture group
m = [Link](r'(\d{4})-(\d{2})-(\d{2})', '2025-03-11')
print([Link](0)) # '2025-03-11' — full match
print([Link](1)) # '2025'
print([Link](2)) # '03'
print([Link](3)) # '11'
print([Link]()) # ('2025','03','11')
# Named groups (?P<name>)
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
m = [Link](pattern, '2025-03-11')
print([Link]('year')) # '2025'
print([Link]()) # {'year':'2025','month':'03','day':'11'}
# ── COMPILED PATTERNS (faster for repeated use) ──
email_re = [Link](
r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
)
emails = email_re.findall('Contact alice@[Link] or bob@[Link]')
print(emails) # ['alice@[Link]', 'bob@[Link]']
# ── COMMON PATTERNS ──
# Email validation
email_valid = [Link](r'^[\w.+-]+@[\w-]+\.[a-z]{2,}$', re.I)
# Phone: +91-98765-43210 or 09876543210
phone_re = [Link](r'(\+91[-\s]?)?[6-9]\d{9}')
# URL
url_re = [Link](r'https?://[\w.-]+(/[\w./-]*)?')
# Indian PIN code
pin_re = [Link](r'^[1-9][0-9]{5}$')
# ── FLAGS ──
# [Link] (re.I) — case-insensitive
# [Link] (re.M) — ^ and $ match line start/end
# [Link] (re.S) — . matches \n too
# [Link] (re.X) — allow whitespace/comments in pattern
# VERBOSE example — readable complex pattern
date_pattern = [Link](r'''
(?P<year> \d{4}) - # 4-digit year
(?P<month> \d{2}) - # 2-digit month
(?P<day> \d{2}) # 2-digit day
''', [Link])
Module 10: json — JSON Encoding and Decoding
▸ json Module — Complete
import json
# ── ENCODING: Python → JSON ──
data = {
'name': 'Alice',
'age': 30,
'scores': [95, 87, 92],
'active': True,
'address': None,
'rating': 4.8
}
# [Link] — dict to string
s = [Link](data) # compact one-liner
s2 = [Link](data, indent=4) # pretty-printed
s3 = [Link](data, indent=2, sort_keys=True) # sorted keys
s4 = [Link](data, separators=(',',':')) # smallest output
print(s2)
# [Link] — dict to file
with open('[Link]', 'w', encoding='utf-8') as f:
[Link](data, f, indent=4, ensure_ascii=False) # allow unicode
# ── DECODING: JSON → Python ──
back = [Link](s)
print(type(back)) # <class 'dict'>
print(back['name']) # Alice
with open('[Link]') as f:
loaded = [Link](f)
# ── JSON ↔ PYTHON TYPE MAPPING ──
# JSON object → Python dict
# JSON array → Python list
# JSON string → Python str
# JSON number → Python int or float
# JSON true → Python True
# JSON false → Python False
# JSON null → Python None
# ── CUSTOM ENCODER ──
from datetime import datetime
from decimal import Decimal
class CustomEncoder([Link]):
def default(self, obj):
if isinstance(obj, datetime):
return [Link]()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, set):
return sorted(list(obj))
# Let parent handle the rest (raises TypeError if unsupported)
return super().default(obj)
event = {
'name': 'Launch',
'time': datetime(2025, 6, 1, 10, 0),
'price': Decimal('99.99'),
'tags': {'python', 'ai', 'ml'}
}
print([Link](event, cls=CustomEncoder, indent=2))
# ── CUSTOM DECODER ──
def decode_datetime(d):
for key, value in [Link]():
if isinstance(value, str):
try:
d[key] = [Link](value)
except ValueError:
pass
return d
raw = '{"event": "Launch", "time": "2025-06-01T10:00:00"}'
parsed = [Link](raw, object_hook=decode_datetime)
print(type(parsed['time'])) # <class '[Link]'>
Module 11: pathlib — Object-Oriented Paths
▸ pathlib Module — Complete
from pathlib import Path
# ── CREATING PATHS ──
p1 = Path('.') # current directory
p2 = Path('/home/user') # absolute path
p3 = [Link]() # user home directory
p4 = [Link]() # current working directory
# ── BUILDING PATHS WITH / OPERATOR ──
project = [Link]() / 'projects' / 'myapp'
config = project / 'config' / '[Link]'
print(config) # /home/user/projects/myapp/config/[Link]
# ── PATH COMPONENTS ──
f = Path('/home/user/docs/[Link]')
print([Link]) # '[Link]'
print([Link]) # 'report'
print([Link]) # '.pdf'
print([Link]) # ['.pdf']
print([Link]) # /home/user/docs
print([Link][0]) # /home/user/docs
print([Link][1]) # /home/user
print([Link][2]) # /home
print([Link]) # '/'
print([Link]) # '' (Linux) or 'C:' (Windows)
print([Link]) # ('/', 'home', 'user', 'docs', '[Link]')
# ── CHECKS ──
print([Link]()) # True/False
print(f.is_file()) # True if regular file
print(f.is_dir()) # True if directory
print(f.is_absolute()) # True if absolute path
print(f.is_symlink()) # True if symbolic link
# ── FILE I/O ──
p = Path('[Link]')
p.write_text('Hello, World!', encoding='utf-8') # write
content = p.read_text(encoding='utf-8') # read
raw = p.read_bytes() # as bytes
p.write_bytes(b'binary data') # write bytes
# ── DIRECTORY OPERATIONS ──
new_dir = Path('output/results')
new_dir.mkdir(parents=True, exist_ok=True) # create with parents
# ── LISTING ──
for entry in Path('.').iterdir():
print([Link], '- file' if entry.is_file() else '- dir')
# Glob patterns
py_files = list(Path('.').glob('*.py')) # current dir only
all_py = list(Path('.').rglob('*.py')) # recursive
configs = list(Path('.').glob('**/*.json')) # all JSON recursively
# ── STAT INFORMATION ──
stat = Path('[Link]').stat()
print(f'Size: {stat.st_size}B')
from datetime import datetime
print(f'Modified: {[Link](stat.st_mtime)}')
# ── RENAMING / MOVING ──
[Link]('new_name.txt') # rename in same directory
[Link]('/tmp/new_name.txt') # move (overwrites destination)
# ── RELATIVE PATHS ──
base = Path('/home/user')
target = Path('/home/user/docs/[Link]')
rel = target.relative_to(base) # PosixPath('docs/[Link]')
print(rel)
# ── RESOLVE ──
sym = Path('shortcut') # could be a symlink
# absolute = [Link]() # get real absolute path
Module 12: shutil — High-Level File Operations
▸ shutil Module
import shutil, os
# ── COPYING FILES ──
[Link]('[Link]', '[Link]') # copy content + permissions
shutil.copy2('[Link]', '[Link]') # copy + preserve timestamps
[Link]('[Link]', '[Link]') # content only
[Link](src_file, dst_file) # between open file objects
# ── COPYING DIRECTORY TREES ──
[Link]('src_dir', 'dst_dir') # copy entire folder tree
[Link]('src', 'dst', ignore=shutil.ignore_patterns('*.pyc',
'__pycache__'))
# ── MOVING ──
[Link]('[Link]', 'subdir/') # move file
[Link]('old_dir/', 'new_dir/') # rename/move directory
# ── DELETING ──
[Link]('dir_to_delete') # delete directory + contents
[Link]('dir', ignore_errors=True) # suppress errors
# ── ARCHIVING ──
# Create zip/tar archives
shutil.make_archive('backup', 'zip', root_dir='./src') # [Link]
shutil.make_archive('backup', 'gztar', root_dir='./src') # [Link]
# Extract archive
shutil.unpack_archive('[Link]', extract_dir='./extracted')
# ── DISK USAGE ──
usage = shutil.disk_usage('/')
print(f'Total: {[Link]/1e9:.1f} GB')
print(f'Used: {[Link]/1e9:.1f} GB')
print(f'Free: {[Link]/1e9:.1f} GB')
print(f'Usage: {[Link]/[Link]*100:.1f}%')
# ── WHICH — find program in PATH ──
python_path = [Link]('python3')
print(python_path) # /usr/bin/python3
Module 13: subprocess — Run External Commands
▸ subprocess Module
import subprocess
# ── SIMPLE APPROACH: [Link]() ──
# Run command, wait for completion
result = [Link](['echo', 'Hello, World!'])
print([Link]) # 0 (success)
# Capture stdout
result = [Link](
['python3', '-c', 'print(42)'],
capture_output=True, # captures both stdout + stderr
text=True # decode bytes to str
)
print([Link]) # '42\n'
print([Link]) # ''
print([Link]) # 0
# Raise exception if command fails
result = [Link](['ls', '/nonexistent'], capture_output=True,
text=True)
# Don't crash — check returncode manually:
if [Link] != 0:
print(f'Error: {[Link]}')
# OR use check=True to auto-raise on failure
try:
[Link](['false'], check=True) # 'false' always fails
except [Link] as e:
print(f'Command failed: {[Link]}')
# ── SHELL COMMANDS ──
# shell=True lets you use shell syntax (pipes, wildcards, etc.)
result = [Link](
'ls -la | head -5',
shell=True, capture_output=True, text=True
)
print([Link])
# ── GET OUTPUT AS STRING ──
output = subprocess.check_output(['date'], text=True)
print([Link]())
# ── TIMEOUT ──
try:
result = [Link](['sleep', '10'], timeout=2)
except [Link]:
print('Command timed out!')
# ── POPEN for real-time I/O ──
# For long-running processes where you need real-time output
with [Link](
['python3', '-c', 'for i in range(3): print(i)'],
stdout=[Link], text=True
) as proc:
for line in [Link]:
print(f'Got: {[Link]()}')
Module 14: logging — Application Logging
▸ logging Module — Complete
import logging
# ── LEVELS (low to high) ──
# DEBUG (10) — detailed diagnostic info
# INFO (20) — confirmation things work as expected
# WARNING (30) — something unexpected but not an error (DEFAULT)
# ERROR (40) — more serious problem
# CRITICAL (50) — program may not continue
# ── BASIC SETUP ──
[Link](
level=[Link],
format='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
[Link]('Debug message') # 2025-03-11 10:00:00 | DEBUG | root |
...
[Link]('Application started')
[Link]('Low disk space')
[Link]('File not found')
[Link]('Database down')
# ── NAMED LOGGERS (best practice) ──
logger = [Link](__name__) # logger named after module
[Link]('This logs with module name')
# ── LOGGING TO FILE + CONSOLE ──
logger2 = [Link]('myapp')
[Link]([Link])
# Console handler
console_handler = [Link]()
console_handler.setLevel([Link]) # only warnings+ on console
# File handler
file_handler = [Link]('[Link]')
file_handler.setLevel([Link]) # all levels in file
# Formatter
fmt = [Link]('%(asctime)s - %(name)s - %(levelname)s - %
(message)s')
console_handler.setFormatter(fmt)
file_handler.setFormatter(fmt)
[Link](console_handler)
[Link](file_handler)
[Link]('Detailed debug info (file only)')
[Link]('Normal operation (file only)')
[Link]('Warning shown on console and saved to file')
# ── LOG EXCEPTIONS ──
try:
x = 1 / 0
except ZeroDivisionError:
[Link]('Math error!', exc_info=True) # logs full traceback
# OR: [Link]('Math error!') — same thing
# ── ROTATING FILE HANDLER ──
from [Link] import RotatingFileHandler
rot_handler = RotatingFileHandler(
'[Link]',
maxBytes=1024*1024, # 1 MB per file
backupCount=5 # keep last 5 rotated files
)
Module 15: statistics — Statistical Functions
▸ statistics Module
import statistics
data = [4, 8, 15, 16, 23, 42, 4, 8, 15, 16]
# ── AVERAGES / CENTRAL TENDENCY ──
print([Link](data)) # 15.1 (arithmetic mean)
print([Link](data)) # 15.5 (middle value)
print(statistics.median_low(data)) # 15 (lower of two midpoints)
print(statistics.median_high(data)) # 16 (higher)
print([Link](data)) # 4 (most frequent)
print([Link](data)) # [4,8,15,16] (all modes)
print(statistics.geometric_mean(data)) # geometric mean
print(statistics.harmonic_mean(data)) # harmonic mean
# ── SPREAD / DISPERSION ──
print([Link](data)) # population std deviation
print([Link](data)) # sample std deviation (Bessel corrected)
print([Link](data)) # population variance
print([Link](data)) # sample variance
# ── QUARTILES (Python 3.8+) ──
print([Link](data, n=4)) # quartiles [Q1, Q2, Q3]
print([Link](data, n=10)) # deciles
# ── CORRELATION & REGRESSION (Python 3.10+) ──
x = [1, 2, 3, 4, 5]
y = [2, 4, 5, 4, 5]
print([Link](x, y)) # 0.889... (Pearson r)
print([Link](x, y)) # 1.5
slope, intercept = statistics.linear_regression(x, y)
print(f'y = {slope:.2f}x + {intercept:.2f}') # best-fit line
# ── NormalDist (Python 3.8+) ──
from statistics import NormalDist
nd = NormalDist(mu=100, sigma=15) # IQ distribution
print([Link](100)) # probability density at 100
print([Link](115)) # P(X <= 115) = 0.841
print(nd.inv_cdf(0.95)) # value at 95th percentile ≈ 124.7
print([Link](NormalDist(110, 15))) # overlap coefficient
Module 16: typing — Type Hints
▸ typing Module — Complete
# Type hints make code more readable and enable IDE checking
# They are NOT enforced at runtime — Python is still dynamic
from typing import (List, Dict, Tuple, Set, Optional,
Union, Any, Callable, Iterator,
TypeVar, Generic, Final, ClassVar)
# ── BASIC ANNOTATIONS ──
name: str = 'Alice'
age: int = 25
scores: list[float] = [9.5, 8.0, 9.0] # Python 3.9+ lowercase
# ── FUNCTION ANNOTATIONS ──
def greet(name: str, times: int = 1) -> str:
return (name + ' ') * times
def process(data: list[int]) -> dict[str, float]:
return {'mean': sum(data)/len(data), 'max': max(data)}
# ── Optional (can be None) ──
def find_user(uid: int) -> Optional[str]: # returns str or None
users = {1: 'Alice', 2: 'Bob'}
return [Link](uid)
# Python 3.10+ syntax: str | None
def find_v2(uid: int) -> str | None:
...
# ── Union ──
def double(x: Union[int, float]) -> Union[int, float]:
return x * 2
# Python 3.10+: int | float
def double_v2(x: int | float) -> int | float:
return x * 2
# ── Callable ──
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
print(apply(lambda a, b: a + b, 3, 4)) # 7
# ── TypeVar — Generic functions ──
T = TypeVar('T')
def first(lst: list[T]) -> T: # works with list of any type
return lst[0]
print(first([1, 2, 3])) # 1 — T is int
print(first(['a', 'b'])) # 'a' — T is str
# ── Final — constant that cannot be reassigned ──
MAX_SIZE: Final = 100
# MAX_SIZE = 200 — mypy/pyright would flag this as error
# ── ClassVar — class-level variable ──
class Config:
DEBUG: ClassVar[bool] = False
name: str
# ── TYPE CHECKING ──
# Use 'mypy' or 'pyright' to check types statically:
# pip install mypy
# mypy [Link]
# ── Literal types (Python 3.8+) ──
from typing import Literal
Mode = Literal['r', 'w', 'a', 'rb', 'wb']
def open_file(path: str, mode: Mode) -> None: ...
# ── TypedDict (Python 3.8+) ──
from typing import TypedDict
class Movie(TypedDict):
title: str
year: int
rating: float
m: Movie = {'title': 'Inception', 'year': 2010, 'rating': 8.8}
Module 17: decimal and fractions — Precise Numbers
▸ decimal and fractions — Precise Arithmetic
# ── FLOATING POINT PROBLEM ──
print(0.1 + 0.2) # 0.30000000000000004 !
print(0.1 + 0.2 == 0.3) # False !
# ── decimal — exact decimal arithmetic ──
from decimal import Decimal, getcontext, ROUND_HALF_UP
# Set global precision
getcontext().prec = 28 # 28 significant digits
a = Decimal('0.1')
b = Decimal('0.2')
print(a + b) # 0.3 — EXACT!
print(a + b == Decimal('0.3')) # True!
# Financial calculation
price = Decimal('999.99')
tax_rate = Decimal('0.18') # 18% GST
tax = (price * tax_rate).quantize(Decimal('0.01'),
rounding=ROUND_HALF_UP)
total = price + tax
print(f'Price: {price}, Tax: {tax}, Total: {total}')
# Price: 999.99, Tax: 180.00, Total: 1179.99
# Decimal vs float precision
from decimal import Decimal as D
print(D('1') / D('3')) # 0.3333333333333333333333333333 (28 digits!)
print(1/3) # 0.3333333333333333 (17 digits float)
# ── fractions — exact rational arithmetic ──
from fractions import Fraction
a = Fraction(1, 3) # 1/3
b = Fraction(1, 6) # 1/6
print(a + b) # 1/2 (auto-simplified!)
print(a * b) # 1/18
print(a - b) # 1/6
# From float (shows why floats are imprecise)
print(Fraction(0.1)) # 3602879701896397/36028797018963968 !
print(Fraction('0.1')) # 1/10 — correct!
# Fraction arithmetic is always exact
result = sum(Fraction(1, k) for k in range(1, 11))
print(result) # 7381/2520 (exact sum of 1+1/2+1/3+...+1/10)
Module 18: time — Time Access and Conversions
▸ time Module
import time
# ── GETTING CURRENT TIME ──
ts = [Link]() # Unix timestamp (float seconds since 1970)
print(ts) # 1741699200.0
ts_ns = time.time_ns() # nanosecond precision
print(ts_ns)
# ── SLEEPING ──
print('Before sleep')
[Link](0.5) # pause 500ms
print('After sleep')
# ── PERFORMANCE MEASUREMENT ──
# time.perf_counter() — highest resolution, for timing code
start = time.perf_counter()
result = sum(range(1_000_000))
end = time.perf_counter()
print(f'Sum: {result}, Time: {(end-start)*1000:.2f}ms')
# time.process_time() — CPU time only (excludes sleep/IO wait)
cpu_start = time.process_time()
# ... do work ...
cpu_end = time.process_time()
cpu_time = cpu_end - cpu_start
# ── STRUCT TIME ──
# [Link]() → struct_time (local time)
# [Link]() → struct_time (UTC)
lt = [Link]() # struct_time object
print(lt.tm_year) # 2025
print(lt.tm_mon) # 3 (March)
print(lt.tm_mday) # 11
print(lt.tm_hour) # hour (24-hour)
print(lt.tm_wday) # 0=Mon...6=Sun
print(lt.tm_yday) # day of year (1-366)
# ── FORMATTING ──
print([Link]('%Y-%m-%d %H:%M:%S')) # format current
print([Link]('%A, %d %B %Y', [Link]())) # custom format
# ── MONOTONIC CLOCK (no drift backward) ──
# Use for measuring elapsed time — unaffected by NTP adjustments
t1 = [Link]()
[Link](0.1)
t2 = [Link]()
print(f'Elapsed: {(t2-t1)*1000:.1f}ms')
Module 19: threading — Concurrent Execution
▸ threading Module
import threading, time
# ── CREATING THREADS ──
def download(url, delay):
print(f'Starting download: {url}')
[Link](delay) # simulate network delay
print(f'Finished: {url}')
# Without threading: sequential (slow)
# download('[Link]', 2) # 2s
# download('[Link]', 3) # 3s — total: 5s
# With threading: concurrent (fast)
t1 = [Link](target=download, args=('[Link]', 2))
t2 = [Link](target=download, args=('[Link]', 3))
[Link]() # start both
[Link]()
[Link]() # wait for both to finish
[Link]()
# Total time: ~3s (not 5s)
# ── THREAD CLASS (OOP style) ──
class Worker([Link]):
def __init__(self, task_id, data):
super().__init__()
self.task_id = task_id
[Link] = data
[Link] = None
def run(self): # override run() — called by start()
print(f'Worker {self.task_id} starting')
[Link] = sum([Link]) # do work
print(f'Worker {self.task_id} done: {[Link]}')
workers = [Worker(i, range(i*1000, (i+1)*1000)) for i in range(4)]
for w in workers: [Link]()
for w in workers: [Link]()
results = [[Link] for w in workers]
print(f'All results: {results}')
# ── LOCK — prevent race conditions ──
counter = 0
lock = [Link]()
def safe_increment():
global counter
with lock: # acquire lock, auto-release on exit
counter += 1
threads = [[Link](target=safe_increment) for _ in range(1000)]
for t in threads: [Link]()
for t in threads: [Link]()
print(f'Counter: {counter}') # 1000 (correct — no race condition)
# ── Python's GIL Note ──
# CPython has a Global Interpreter Lock (GIL)
# Threads run concurrently but not truly in parallel (for CPU tasks)
# For I/O-bound: threading works well (network, file, DB)
# For CPU-bound: use multiprocessing instead