# 🐍 Python — Complete Content Reference | PART 1
Sections 1–8: Fundamentals, Control Flow, Functions, Strings, Lists, Tuples, Dicts, Sets
Har topic ka full content — theory + code + gotchas. Kuch nahi chhodha!
# 📦 SECTION 1 — Python Fundamentals
# 1.1 Introduction
# What is Python?
Python is a high-level, interpreted, dynamically-typed, general-purpose programming language. It emphasizes readability and simplicity — "There should be one obvious way to do it."
- Interpreted → runs line by line via Python interpreter
- Dynamically typed → type checking happens at runtime, not compile time
- Garbage collected → automatic memory management
- Multi-paradigm → supports OOP, functional, procedural styles
# History
- Created by Guido van Rossum, released in 1991
- Named after Monty Python's Flying Circus (not the snake!)
- Python 2 → legacy, end-of-life since January 1, 2020
- Python 3 → current, always use 3.x (latest: 3.12+)
# Python 2 vs Python 3
| Feature | Python 2 | Python 3 |
|---|---|---|
print |
statement: print "hi" |
function: print("hi") |
| Integer division | 5/2 = 2 |
5/2 = 2.5 |
unicode |
separate type | default str |
range() |
returns list | returns iterator |
input() |
raw_input() used | input() |
# Python Interpreters
| Interpreter | Description |
|---|---|
| CPython | Default, written in C. Most used |
| PyPy | JIT compiled Python — 5-10x faster for loops |
| Jython | Python on JVM (Java Virtual Machine) |
| IronPython | Python on .NET CLR |
| MicroPython | Python for microcontrollers (ESP32, Arduino) |
# How Python Runs
plaintext
your_code.py
↓
[Python Interpreter - CPython]
↓
Compilation → Bytecode (.pyc files in __pycache__/)
↓
Python Virtual Machine (PVM) executes bytecode
↓
Output
python
# See bytecode yourself!
import dis
def add(a, b):
return a + b
dis.dis(add)
# Output shows bytecode instructions: LOAD_FAST, BINARY_ADD, etc.
# REPL (Read-Eval-Print Loop)
bash
$ python3 # start REPL
>>> 2 + 2 # Read
4 # Eval + Print
>>> exit() # Loop
# Installing Python & pip
bash
# Check version
python3 --version
pip3 --version
# Install a package
pip install requests
# Install specific version
pip install requests==2.28.0
# Install from requirements file
pip install -r requirements.txt
# Upgrade pip itself
pip install --upgrade pip
# List installed packages
pip list
# Show package info
pip show requests
# Uninstall
pip uninstall requests
# Virtual Environments ⭐ (ALWAYS USE THIS!)
bash
# Create venv
python3 -m venv myenv
# Activate (Linux/Mac)
source myenv/bin/activate
# Activate (Windows)
myenv\Scripts\activate
# Deactivate
deactivate
# With conda
conda create -n myenv python=3.11
conda activate myenv
# With poetry (modern, recommended)
poetry new myproject
poetry add requests
poetry install
# Running Scripts
bash
python3 script.py
python3 -m module_name # run as module
python3 -c "print('Hello')" # one-liner
# `__name__ == "__main__"` Idiom ⭐
python
# module.py
def greet(name):
return f"Hello, {name}!"
# This runs ONLY when script is executed directly
# NOT when imported as a module
if __name__ == "__main__":
print(greet("Bhai"))
# Why it matters:
# import module → __name__ == "module" → if block SKIPPED
# python module.py → __name__ == "__main__" → if block RUNS
# PEP 8 — Style Guide (Must Follow!)
python
# ✅ Correct PEP 8
def calculate_total(price, tax_rate):
"""Calculate total price with tax."""
return price * (1 + tax_rate)
MY_CONSTANT = 42 # UPPER_SNAKE_CASE for constants
my_variable = "hello" # snake_case for variables
MyClass = "ClassName" # PascalCase for classes
_private_var = "private" # single underscore = convention for private
# ❌ Wrong
def CalculateTotal(Price,TaxRate): # CamelCase for functions = WRONG
return Price*(1+TaxRate)
# Spaces
x = 1 + 2 # ✅ spaces around operators
x=1+2 # ❌ no spaces
# Line length: max 79 characters (PEP 8) or 88 (Black formatter)
# PEP 20 — The Zen of Python
python
import this
# Beautiful is better than ugly.
# Explicit is better than implicit.
# Simple is better than complex.
# Readability counts.
# There should be one obvious way to do it.
# ...
# 1.2 Variables & Data Types
# Variables (Dynamic Typing)
python
x = 10 # int
x = "hello" # now str — Python allows this! (dynamic typing)
x = [1, 2, 3] # now list
# Multiple assignment
a = b = c = 0
a, b, c = 1, 2, 3 # tuple unpacking
a, b = b, a # swap — Pythonic!
# type() — check type
print(type(42)) #
print(type("hi")) #
print(type([1,2])) #
print(type(None)) #
# id() — memory address
x = 10
print(id(x)) # some memory address
y = x
print(id(y) == id(x)) # True — same object in memory!
# Primitive Types
# `int` — Arbitrary Precision!
python
# Python ints have NO fixed size — can be astronomically large!
x = 10
y = -5
big = 10 ** 100 # no overflow! (unlike C/Java)
print(big) # googol!
# Integer literals
decimal = 1_000_000 # underscores for readability
binary = 0b1010 # 10
octal = 0o17 # 15
hexa = 0xFF # 255
# `float` — IEEE 754
python
x = 3.14
y = 1.5e10 # scientific notation = 15000000000.0
z = float('inf') # infinity
n = float('nan') # Not a Number
# GOTCHA — float precision!
print(0.1 + 0.2) # 0.30000000000000004 — NOT 0.3!
print(0.1 + 0.2 == 0.3) # False !!
# Fix: use round() or decimal module
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2')) # 0.3 ✅
# `complex`
python
z = 3 + 4j
print(z.real) # 3.0
print(z.imag) # 4.0
print(abs(z)) # 5.0 (magnitude)
print(z.conjugate()) # (3-4j)
# `bool` — Subclass of int!
python
print(True == 1) # True — bool IS an int!
print(False == 0) # True
print(True + True) # 2 (!!!)
print(isinstance(True, int)) # True
# Truthy and Falsy
# FALSY: False, None, 0, 0.0, 0j, "", [], (), {}, set()
# TRUTHY: everything else
if []:
print("truthy") # not printed — empty list is falsy
if [0]:
print("truthy") # printed — list with item is truthy!
# `str`
python
s = "hello"
s2 = 'world'
s3 = """multi
line"""
# `NoneType`
python
x = None
print(x is None) # True ✅ (always use 'is' not '==' for None)
print(x == None) # True but BAD PRACTICE ❌
def func():
pass # implicitly returns None
result = func()
print(result) # None
# Collection Types
python
# list — ordered, mutable, duplicates allowed
my_list = [1, 2, 3, "hi", True]
# tuple — ordered, immutable, duplicates allowed
my_tuple = (1, 2, 3)
# dict — key-value pairs, ordered (Python 3.7+), mutable
my_dict = {"name": "Bhai", "age": 20}
# set — unordered, mutable, no duplicates
my_set = {1, 2, 3, 3} # {1, 2, 3} — 3 appears once
# frozenset — immutable set (can be dict key!)
fs = frozenset({1, 2, 3})
# bytes
b = b"hello" # bytes literal
# bytearray — mutable bytes
ba = bytearray(b"hello")
ba[0] = 72 # can modify!
# memoryview — zero-copy view of bytes-like objects
mv = memoryview(b"hello")
# Type Conversion
python
# Implicit (automatic)
x = 5 + 2.0 # int + float → float (2.0)
# Explicit (manual)
int("42") # 42
int(3.9) # 3 (truncates, does NOT round!)
float("3.14") # 3.14
str(42) # "42"
bool(0) # False
bool("hello") # True
list((1, 2, 3)) # [1, 2, 3]
tuple([1, 2, 3]) # (1, 2, 3)
set([1, 2, 2, 3]) # {1, 2, 3}
dict([("a", 1), ("b", 2)]) # {"a": 1, "b": 2}
# isinstance() — type check
print(isinstance(42, int)) # True
print(isinstance(42, (int, float))) # True — checks multiple types!
print(isinstance(True, int)) # True — bool is subclass of int
# issubclass()
print(issubclass(bool, int)) # True
print(issubclass(int, float)) # False
# 1.3 Operators
# Arithmetic
python
print(10 + 3) # 13 — addition
print(10 - 3) # 7 — subtraction
print(10 * 3) # 30 — multiplication
print(10 / 3) # 3.333... — true division (always float!)
print(10 // 3) # 3 — floor division (integer result)
print(10 % 3) # 1 — modulo (remainder)
print(2 ** 10) # 1024 — exponentiation
# Floor division with negatives — watch out!
print(-10 // 3) # -4 (floors towards -infinity, NOT towards zero!)
print(-10 % 3) # 2 (not -1!)
# Comparison
python
x = 5
print(x == 5) # True
print(x != 5) # False
print(x > 3) # True
print(x < 10) # True
print(x >= 5) # True
print(x <= 4) # False
# Chaining comparisons — Python specific!
print(1 < x < 10) # True — equivalent to (1 < x) and (x < 10)
print(1 < x < 4) # False
# Logical
python
print(True and False) # False
print(True or False) # True
print(not True) # False
# Short-circuit evaluation ⭐
# 'and' stops at first False
# 'or' stops at first True
x = None
y = x and x.strip() # safe — won't call .strip() if x is None
# 'and'/'or' return OPERANDS, not just booleans!
print(0 or "default") # "default"
print("hello" or "default") # "hello"
print(None and "value") # None
print("a" and "b") # "b"
# Bitwise
python
a = 0b1100 # 12
b = 0b1010 # 10
print(a & b) # 0b1000 = 8 (AND)
print(a | b) # 0b1110 = 14 (OR)
print(a ^ b) # 0b0110 = 6 (XOR)
print(~a) # -13 (NOT — inverts all bits)
print(a << 2) # 48 (left shift = multiply by 4)
print(a >> 1) # 6 (right shift = divide by 2)
# Common bit tricks
n = 42
print(n & 1) # 0 → even, 1 → odd
print(n & (n-1)) # 0 → power of 2
print(n | (1 << 3)) # set bit 3
print(n & ~(1 << 3)) # clear bit 3
print(n ^ (1 << 3)) # toggle bit 3
# Assignment Operators
python
x = 10
x += 5 # x = 15
x -= 3 # x = 12
x *= 2 # x = 24
x /= 4 # x = 6.0
x //= 2 # x = 3.0
x **= 3 # x = 27.0
x %= 5 # x = 2.0
x &= 3 # bitwise AND
x |= 4 # bitwise OR
x ^= 1 # bitwise XOR
# Identity Operators
python
# 'is' checks if SAME OBJECT (same memory address)
# '==' checks if SAME VALUE
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True — same value
print(a is b) # False — different objects!
print(a is c) # True — c points to same object as a
# Use 'is' for: None, True, False (singletons)
x = None
print(x is None) # ✅ correct
print(x == None) # ❌ works but bad practice
# Membership Operators
python
lst = [1, 2, 3, 4, 5]
print(3 in lst) # True
print(6 not in lst) # True
# Works on strings, dicts (checks keys), sets
print("h" in "hello") # True
print("name" in {"name": "x"}) # True (checks keys)
print(2 in {1, 2, 3}) # True
# Walrus Operator (`:=`) — Python 3.8+
python
# Assignment expression — assigns AND returns value
import re
# Without walrus
data = input("Enter: ")
if len(data) > 10:
print(f"Too long: {len(data)}")
# With walrus ✅
if (n := len(data := input("Enter: "))) > 10:
print(f"Too long: {n}")
# Great in while loops
while chunk := file.read(8192):
process(chunk)
# In list comprehension with filter
results = [y for x in data if (y := process(x)) is not None]
# Operator Precedence (High → Low)
python
# () Parentheses — highest
# ** Exponentiation (right-to-left!)
# +x, -x, ~x Unary operators
# *, /, //, % Multiplication, division
# +, - Addition, subtraction
# <<, >> Bit shifts
# & Bitwise AND
# ^ Bitwise XOR
# | Bitwise OR
# ==, !=, <, >, <=, >=, is, in Comparisons
# not Logical NOT
# and Logical AND
# or Logical OR
# := Walrus (lowest)
# Example
result = 2 + 3 * 4 ** 2 # 2 + 3*16 = 2+48 = 50
result = (2 + 3) * 4 ** 2 # 5 * 16 = 80
# 🔁 SECTION 2 — Control Flow
# 2.1 Conditionals
python
# Basic if-elif-else
age = 20
if age < 13:
print("Child")
elif age < 18:
print("Teen")
elif age < 65:
print("Adult")
else:
print("Senior")
# Nested conditionals
x = 5
if x > 0:
if x > 10:
print("Greater than 10")
else:
print("Between 0 and 10")
else:
print("Non-positive")
# Truthy & Falsy Values
python
# FALSY (evaluate to False in boolean context)
falsy = [False, None, 0, 0.0, 0j, "", [], (), {}, set(), frozenset()]
for val in falsy:
if not val:
print(f"{val!r} is falsy")
# TRUTHY — everything else
# Non-zero numbers, non-empty collections, non-None objects
# Pythonic use
my_list = []
if my_list: # ✅ Pythonic
print("has items")
if len(my_list) > 0: # ❌ Not Pythonic
print("has items")
# Ternary (Conditional Expression)
python
x = 10
# Ternary syntax: value_if_true if condition else value_if_false
result = "positive" if x > 0 else "non-positive"
print(result) # positive
# Nested ternary (use sparingly — readability!)
sign = "positive" if x > 0 else "negative" if x < 0 else "zero"
# Common use: default values
name = user_input if user_input else "Anonymous"
name = user_input or "Anonymous" # same thing using 'or'!
# `match` Statement — Python 3.10+ ⭐
python
# Structural Pattern Matching — Python's switch on steroids!
command = "quit"
match command:
case "quit" | "exit":
print("Goodbye!")
case "hello":
print("Hi there!")
case _: # default (like 'default' in switch)
print("Unknown command")
# Match with types and destructuring
point = (1, 0)
match point:
case (0, 0):
print("Origin")
case (x, 0):
print(f"On x-axis at x={x}")
case (0, y):
print(f"On y-axis at y={y}")
case (x, y):
print(f"Point at ({x}, {y})")
# Match with classes
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
match p:
case Point(x=0, y=0):
print("Origin")
case Point(x=x_val, y=y_val):
print(f"Point at ({x_val}, {y_val})")
# Match with guards
value = 15
match value:
case x if x < 0:
print("negative")
case x if x < 10:
print("small")
case x:
print(f"large: {x}")
# 2.2 Loops
# `for` Loop
python
# Iterate over any iterable
for i in range(5):
print(i) # 0, 1, 2, 3, 4
# Over list
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# Over string
for char in "Python":
print(char)
# Over dict
person = {"name": "Alice", "age": 30}
for key in person: # iterates over keys
print(key, person[key])
for key, value in person.items(): # ✅ Pythonic
print(f"{key}: {value}")
# Over set (no guaranteed order!)
for item in {1, 2, 3}:
print(item)
# `while` Loop
python
count = 0
while count < 5:
print(count)
count += 1
# Infinite loop with break
while True:
user_input = input("Enter 'quit' to exit: ")
if user_input == "quit":
break
print(f"You entered: {user_input}")
# `break`, `continue`, `pass`
python
# break — exit loop immediately
for i in range(10):
if i == 5:
break
print(i) # 0, 1, 2, 3, 4
# continue — skip to next iteration
for i in range(10):
if i % 2 == 0:
continue
print(i) # 1, 3, 5, 7, 9
# pass — do nothing (placeholder)
for i in range(5):
pass # valid empty loop
class EmptyClass:
pass # valid empty class
# `else` Clause on Loops ⭐ (Unique to Python!)
python
# else runs if loop completed WITHOUT hitting break
for i in range(5):
if i == 10: # never true
break
else:
print("Loop finished normally!") # THIS RUNS
# Practical use: searching
numbers = [1, 3, 5, 7, 9]
for num in numbers:
if num == 4:
print("Found 4!")
break
else:
print("4 not found") # runs because 4 was never found
# Same with while
n = 10
while n > 0:
if n == 5:
break
n -= 1
else:
print("while completed without break")
# `range()`
python
range(stop) # 0 to stop-1
range(start, stop) # start to stop-1
range(start, stop, step) # with step
for i in range(5): # 0,1,2,3,4
for i in range(1, 6): # 1,2,3,4,5
for i in range(0, 10, 2): # 0,2,4,6,8
for i in range(10, 0, -1): # 10,9,8,7,6,5,4,3,2,1
# range is lazy — doesn't create list in memory!
r = range(1_000_000) # takes almost no memory
print(list(r[:5])) # [0, 1, 2, 3, 4]
# `enumerate()` ⭐
python
fruits = ["apple", "banana", "cherry"]
# Without enumerate (bad)
for i in range(len(fruits)):
print(i, fruits[i])
# With enumerate (Pythonic!) ✅
for i, fruit in enumerate(fruits):
print(i, fruit)
# Custom start index
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry
# `zip()` ⭐
python
names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
grades = ["A", "B", "A"]
# Combine iterables in parallel
for name, score in zip(names, scores):
print(f"{name}: {score}")
# zip returns tuples
pairs = list(zip(names, scores))
print(pairs) # [('Alice', 95), ('Bob', 87), ('Charlie', 92)]
# Unzip
names2, scores2 = zip(*pairs) # unzip using * operator
# zip stops at shortest iterable!
a = [1, 2, 3]
b = [10, 20]
print(list(zip(a, b))) # [(1, 10), (2, 20)] — 3 dropped!
# zip_longest — continues with fillvalue
from itertools import zip_longest
print(list(zip_longest(a, b, fillvalue=0)))
# [(1, 10), (2, 20), (3, 0)]
# zip 3+ iterables
for name, score, grade in zip(names, scores, grades):
print(f"{name}: {score} ({grade})")
# 🧩 SECTION 3 — Functions
# 3.1 Basics
python
# Define a function
def greet(name):
"""Greet a person by name. ← docstring"""
return f"Hello, {name}!"
result = greet("Bhai")
print(result) # Hello, Bhai!
# Function with no return → returns None implicitly
def say_hi():
print("Hi!")
x = say_hi() # prints "Hi!"
print(x) # None
# Default Arguments
python
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Alice")) # Hello, Alice!
print(greet("Bob", "Hey")) # Hey, Bob!
# ⚠️ GOTCHA: Mutable default arguments — BIGGEST Python bug!
def append_to(element, to=[]): # ❌ WRONG!
to.append(element)
return to
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] — list is shared between calls!
print(append_to(3)) # [1, 2, 3] — SHOCKING! 😱
# ✅ CORRECT way
def append_to(element, to=None):
if to is None:
to = []
to.append(element)
return to
print(append_to(1)) # [1]
print(append_to(2)) # [2] — fresh list each time ✅
# Keyword Arguments
python
def make_coffee(size, milk=False, sugar=0, type="espresso"):
return f"{size} {type} | milk={milk} | sugar={sugar}"
# Can pass in any order using keywords
print(make_coffee("large", sugar=2, milk=True))
print(make_coffee(size="small", type="latte", milk=True))
# Positional-Only Parameters (`/`)
python
# Parameters before / can ONLY be positional, not keyword
def add(x, y, /):
return x + y
add(1, 2) # ✅
add(x=1, y=2) # ❌ TypeError: got unexpected keyword argument
# Keyword-Only Parameters (`*`)
python
# Parameters after * can ONLY be keyword, not positional
def greet(name, *, formal=False):
if formal:
return f"Good day, {name}"
return f"Hey, {name}!"
greet("Alice") # ✅ Hey, Alice!
greet("Alice", formal=True) # ✅ Good day, Alice
greet("Alice", True) # ❌ TypeError
# `*args` and `**kwargs`
python
# *args — variable positional arguments → tuple
def sum_all(*args):
print(type(args)) #
return sum(args)
print(sum_all(1, 2, 3, 4, 5)) # 15
# **kwargs — variable keyword arguments → dict
def show_info(**kwargs):
print(type(kwargs)) #
for key, value in kwargs.items():
print(f"{key}: {value}")
show_info(name="Alice", age=30, city="NYC")
# Combined
def everything(pos1, pos2, *args, kw_only, **kwargs):
print(pos1, pos2) # positional
print(args) # extra positionals
print(kw_only) # keyword-only
print(kwargs) # extra keywords
everything(1, 2, 3, 4, kw_only="x", a="b", c="d")
# Argument Order Rule
python
# ORDER MUST BE:
# positional / normal * keyword_only **kwargs
def func(pos_only, /, normal, *args, kw_only, **kwargs):
pass
# 3.2 Advanced Functions
# First-Class Functions & Higher-Order Functions
python
# Functions are objects — can be stored, passed, returned!
def square(x):
return x ** 2
func = square # store function in variable
print(func(5)) # 25
functions = [square, abs, str] # store in list
for f in functions:
print(f(-4)) # 16, 4, '-4'
# Higher-order function — takes/returns a function
def apply(func, value):
return func(value)
print(apply(square, 5)) # 25
print(apply(str, 42)) # "42"
# apply to collection
numbers = [1, -2, 3, -4]
print(list(map(abs, numbers))) # [1, 2, 3, 4]
print(list(filter(lambda x: x > 0, numbers))) # [1, 3]
# Lambda Functions
python
# lambda: anonymous one-liner function
square = lambda x: x ** 2
add = lambda x, y: x + y
greet = lambda name: f"Hello, {name}"
print(square(5)) # 25
print(add(2, 3)) # 5
# Best used inline — sorting, map, filter
students = [("Alice", 90), ("Bob", 75), ("Charlie", 88)]
students.sort(key=lambda x: x[1]) # sort by score
students.sort(key=lambda x: x[1], reverse=True) # descending
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
# Closures ⭐
python
# A closure is a function that REMEMBERS its enclosing scope's variables
def make_counter(start=0):
count = start # this is captured in closure
def counter():
nonlocal count # needed to modify outer variable
count += 1
return count
return counter # return the inner function!
counter1 = make_counter()
counter2 = make_counter(10)
print(counter1()) # 1
print(counter1()) # 2
print(counter1()) # 3
print(counter2()) # 11 — independent!
# Closure cells
print(counter1.__closure__) # closure tuple
print(counter1.__closure__[0].cell_contents) # current value
# Real-world use: factory functions
def multiplier(factor):
return lambda x: x * factor
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# `nonlocal` and `global`
python
# global — access/modify module-level variable
count = 0
def increment():
global count # declare intent to modify global
count += 1
increment()
print(count) # 1
# nonlocal — access/modify enclosing scope variable (in closures)
def outer():
x = 10
def inner():
nonlocal x # modify outer's x
x += 5
inner()
print(x) # 15
outer()
# Recursion + Memoization
python
# Basic recursion — MUST have a base case!
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive case
print(factorial(5)) # 120
# ⚠️ Default recursion limit = 1000
import sys
print(sys.getrecursionlimit()) # 1000
sys.setrecursionlimit(5000) # increase if needed
# Memoization — cache results to avoid recomputation
def fib_slow(n):
if n <= 1: return n
return fib_slow(n-1) + fib_slow(n-2) # exponential time!
# With lru_cache — instant memoization ⭐
from functools import lru_cache, cache
@lru_cache(maxsize=None) # None = unlimited cache
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2) # now O(n)!
print(fib(100)) # instantly!
print(fib.cache_info()) # hits, misses, maxsize, currsize
# @cache — simpler, Python 3.9+
@cache
def fib2(n):
if n <= 1: return n
return fib2(n-1) + fib2(n-2)
# `functools.partial`
python
from functools import partial
def power(base, exponent):
return base ** exponent
# Create specialized version with partial
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(3)) # 27
# Practical use
import os
join_with_root = partial(os.path.join, "/root/project")
print(join_with_root("src", "main.py")) # /root/project/src/main.py
# `functools.reduce`
python
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# reduce(function, iterable, initializer)
product = reduce(lambda x, y: x * y, numbers)
print(product) # 120 (1*2*3*4*5)
total = reduce(lambda x, y: x + y, numbers, 0)
print(total) # 15
# Flatten nested list
nested = [[1, 2], [3, 4], [5, 6]]
flat = reduce(lambda a, b: a + b, nested)
print(flat) # [1, 2, 3, 4, 5, 6]
# Function Annotations & Type Hints
python
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
# Annotations don't enforce types at runtime (Python is dynamic)
# But tools like mypy, pyright use them for static analysis
# Access annotations
print(greet.__annotations__)
# {'name': , 'times': , 'return': }
# `functools.wraps`
python
from functools import wraps
def my_decorator(func):
@wraps(func) # ⭐ preserves original function's metadata
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f"Hello, {name}"
# Without @wraps, __name__ and __doc__ would be "wrapper"!
print(greet.__name__) # "greet" ✅ (not "wrapper")
print(greet.__doc__) # "Greet someone." ✅
# 🔡 SECTION 4 — Strings
# String Creation
python
s1 = "double quotes"
s2 = 'single quotes'
s3 = """triple
double quotes — multiline"""
s4 = '''triple
single quotes'''
# Raw strings — backslash is literal
path = r"C:\Users\Name\Documents"
regex = r"\d+\.\d+"
# Byte strings
b = b"bytes"
print(type(b)) #
# f-Strings ⭐ (Python 3.6+)
python
name = "Bhai"
score = 95.567
# Basic
print(f"Hello, {name}!") # Hello, Bhai!
# Expressions
print(f"2 + 2 = {2 + 2}") # 2 + 2 = 4
# Method calls
print(f"{name.upper()}") # BHAI
# Format spec
print(f"{score:.2f}") # 95.57
print(f"{score:>10.2f}") # ' 95.57' (right-align, width 10)
print(f"{1000000:,}") # 1,000,000 (thousands separator)
print(f"{255:#x}") # 0xff (hex)
print(f"{0.75:.1%}") # 75.0%
# Conversion flags
print(f"{name!r}") # 'Bhai' (repr)
print(f"{name!s}") # Bhai (str, default)
print(f"{name!a}") # 'Bhai' (ascii)
# Debug — Python 3.8+ ⭐
x = 42
print(f"{x=}") # x=42 (prints variable name + value)
print(f"{x + 1=}") # x + 1=43
# Multiline f-strings
message = (
f"Name: {name}\n"
f"Score: {score:.2f}\n"
f"Grade: {'A' if score >= 90 else 'B'}"
)
# String Immutability
python
s = "hello"
# s[0] = 'H' # ❌ TypeError: strings are immutable!
s = s.replace("hello", "Hello") # ✅ creates new string
s = "H" + s[1:] # ✅ string slicing
# String Indexing & Slicing
python
s = "Python"
# 012345 (positive indices)
# -654321 (negative indices)
print(s[0]) # P
print(s[-1]) # n (last char)
print(s[1:4]) # yth (1 inclusive, 4 exclusive)
print(s[::2]) # Pto (every 2nd char)
print(s[::-1]) # nohtyP (reverse!)
print(s[2:]) # thon (from index 2 to end)
print(s[:3]) # Pyt (from start to index 3)
# Important String Methods
python
s = " Hello, World! "
# Case
print(s.upper()) # " HELLO, WORLD! "
print(s.lower()) # " hello, world! "
print(s.title()) # " Hello, World! "
print(s.capitalize()) # " hello, world! " → only first char
print(s.swapcase()) # " hELLO, wORLD! "
# Strip whitespace/chars
print(s.strip()) # "Hello, World!" (both sides)
print(s.lstrip()) # "Hello, World! " (left only)
print(s.rstrip()) # " Hello, World!" (right only)
print("***hello***".strip("*")) # "hello" (strip specific chars)
# Split
print("a,b,c".split(",")) # ['a', 'b', 'c']
print("a b c".split()) # ['a', 'b', 'c'] (whitespace)
print("a,b,c".split(",", 1)) # ['a', 'b,c'] (max splits)
print("a\nb\nc".splitlines()) # ['a', 'b', 'c']
print("a,b,c".rsplit(",", 1)) # ['a,b', 'c'] (from right)
# Join ⭐ (use this, NOT + in loop!)
words = ["Hello", "World"]
print(" ".join(words)) # "Hello World"
print(",".join(words)) # "Hello,World"
print("".join(words)) # "HelloWorld"
# Find / Search
s = "hello world hello"
print(s.find("hello")) # 0 (first occurrence, -1 if not found)
print(s.rfind("hello")) # 12 (last occurrence)
print(s.index("hello")) # 0 (like find but raises ValueError if not found!)
print(s.count("hello")) # 2
# Replace
print(s.replace("hello", "hi")) # "hi world hi"
print(s.replace("hello", "hi", 1)) # "hi world hello" (max 1 replacement)
# Check
print("hello".startswith("hel")) # True
print("hello".endswith("llo")) # True
print("hello123".isalnum()) # True
print("hello".isalpha()) # True
print("123".isdigit()) # True
print(" ".isspace()) # True
print("HELLO".isupper()) # True
print("hello".islower()) # True
# Alignment / Padding
print("hi".center(10)) # " hi "
print("hi".ljust(10)) # "hi "
print("hi".rjust(10)) # " hi"
print("42".zfill(5)) # "00042"
# Encode/Decode
b = "hello".encode("utf-8") # b'hello'
s = b.decode("utf-8") # "hello"
# translate + maketrans
table = str.maketrans("aeiou", "*****") # vowels → *
print("hello world".translate(table)) # "h*ll* w*rld"
# `textwrap` Module
python
import textwrap
text = "Python is a great programming language for beginners and experts alike."
print(textwrap.wrap(text, width=30)) # list of wrapped lines
print(textwrap.fill(text, width=30)) # single string with \n
print(textwrap.dedent("""
hello
world
""")) # removes common leading whitespace
# 📋 SECTION 5 — Lists
# Creating & Basic Operations
python
# Creating
empty = []
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", True, 3.14, None]
nested = [[1, 2], [3, 4], [5, 6]]
from_range = list(range(5)) # [0, 1, 2, 3, 4]
repeated = [0] * 5 # [0, 0, 0, 0, 0]
# Indexing & Slicing
lst = [10, 20, 30, 40, 50]
print(lst[0]) # 10
print(lst[-1]) # 50
print(lst[1:3]) # [20, 30]
print(lst[::2]) # [10, 30, 50]
print(lst[::-1]) # [50, 40, 30, 20, 10]
# Mutability — can modify in place!
lst[0] = 100
lst[1:3] = [200, 300]
print(lst) # [100, 200, 300, 40, 50]
# List Methods
python
lst = [3, 1, 4, 1, 5, 9, 2, 6]
# Add elements
lst.append(7) # add to end — O(1)
lst.extend([8, 9, 10]) # add multiple — O(k)
lst.insert(0, 0) # insert at index — O(n)
# Remove elements
lst.remove(1) # remove first occurrence — O(n)
popped = lst.pop() # remove & return last — O(1)
popped = lst.pop(0) # remove & return at index — O(n)
lst.clear() # remove all
# Search
print(lst.index(4)) # index of first occurrence
print(lst.count(1)) # how many times 1 appears
# Sort
lst.sort() # in-place, ascending
lst.sort(reverse=True) # descending
lst.sort(key=lambda x: abs(x)) # custom key
# sorted() — returns NEW sorted list (original unchanged)
new_sorted = sorted(lst)
new_sorted = sorted(lst, reverse=True)
new_sorted = sorted(lst, key=len) # sort strings by length
# Reverse
lst.reverse() # in-place
reversed_lst = list(reversed(lst)) # creates new
# Copy
shallow = lst.copy() # shallow copy
import copy
deep = copy.deepcopy(lst) # deep copy
# `sort()` vs `sorted()` — KEY DIFFERENCE
python
numbers = [3, 1, 4, 1, 5]
# .sort() — modifies IN PLACE, returns None
numbers.sort()
# print(numbers.sort()) # None — common mistake!
# sorted() — returns NEW list, original UNCHANGED
original = [3, 1, 4]
new = sorted(original)
print(original) # [3, 1, 4] — unchanged!
print(new) # [1, 3, 4]
# List Comprehension ⭐
python
# [expression for item in iterable if condition]
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
upper_words = [word.upper() for word in ["hello", "world"]]
filtered = [x for x in range(-5, 5) if x != 0]
# Nested comprehension
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]]
flat = [x for row in nested for x in row]
# [1, 2, 3, 4, 5, 6]
# With multiple conditions
result = [x for x in range(100) if x % 2 == 0 if x % 3 == 0]
# multiples of 6: [0, 6, 12, 18, ...]
# vs traditional loop (comprehension is FASTER!)
# Time: [x**2 for x in range(1000)] is faster than loop
# Unpacking
python
a, b, c = [1, 2, 3]
# Extended unpacking — Python 3+
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
first, *middle, last = [1, 2, 3, 4, 5]
print(middle) # [2, 3, 4]
# Unpack in function call
def add(a, b, c):
return a + b + c
args = [1, 2, 3]
print(add(*args)) # 6
# Shallow vs Deep Copy
python
import copy
# Shallow copy — copies the list, but NOT nested objects
original = [[1, 2], [3, 4]]
shallow = original.copy() # or copy.copy(original) or original[:]
shallow[0].append(99) # modifies the inner list!
print(original) # [[1, 2, 99], [3, 4]] — AFFECTED! 😱
# Deep copy — fully independent
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original) # [[1, 2], [3, 4]] — SAFE ✅
# List as Stack and Queue
python
# Stack (LIFO) — use append/pop
stack = []
stack.append(1) # push
stack.append(2)
stack.append(3)
print(stack.pop()) # 3 — LIFO
# Queue (FIFO) — DON'T use list, use deque!
from collections import deque
queue = deque()
queue.append(1) # enqueue
queue.append(2)
queue.append(3)
print(queue.popleft()) # 1 — FIFO ✅
# list.pop(0) is O(n), deque.popleft() is O(1) ⭐
# 📦 SECTION 6 — Tuples
# Creating Tuples
python
empty = ()
single = (42,) # ⚠️ MUST have trailing comma! (42) is just parentheses
t = (1, 2, 3)
t2 = 1, 2, 3 # parentheses optional!
mixed = (1, "hi", True, 3.14)
# Immutability
t = (1, 2, 3)
# t[0] = 10 # ❌ TypeError: tuple is immutable
# Packing & Unpacking
python
# Packing
point = 3, 4 # packing
x, y = point # unpacking
# Swap using tuple unpacking
a, b = 1, 2
a, b = b, a # elegant swap!
# Extended unpacking
first, *rest = (1, 2, 3, 4, 5)
*init, last = (1, 2, 3, 4, 5)
a, *b, c = (1, 2, 3, 4, 5)
# Returning multiple values from function (returns tuple!)
def min_max(lst):
return min(lst), max(lst) # returns tuple
minimum, maximum = min_max([3, 1, 4, 1, 5, 9])
# Named Tuples ⭐
python
from collections import namedtuple
# Define a named tuple class
Point = namedtuple("Point", ["x", "y"])
Person = namedtuple("Person", "name age city") # space-separated also works
p = Point(3, 4)
print(p.x, p.y) # 3 4 — attribute access!
print(p[0], p[1]) # 3 4 — index access also works
print(p) # Point(x=3, y=4) — nice repr!
person = Person("Alice", 30, "NYC")
print(person.name) # Alice
# Immutable like tuple
# person.name = "Bob" # ❌ AttributeError
# _replace — returns modified copy
older = person._replace(age=31)
print(older) # Person(name='Alice', age=31, city='NYC')
# _fields — all field names
print(Person._fields) # ('name', 'age', 'city')
# _asdict — convert to dict
print(person._asdict()) # {'name': 'Alice', 'age': 30, 'city': 'NYC'}
# Tuple vs List — When to Use Which?
python
# Use TUPLE when:
# - Data shouldn't change (coordinates, RGB color, DB record)
# - Can be used as dict key (hashable!)
# - Slight performance advantage over list
# - Communicate "this data is fixed"
# Use LIST when:
# - Data will be modified (add/remove items)
# - Need .sort(), .append(), etc.
# Tuple as dict key
locations = {
(40.7128, -74.0060): "New York",
(51.5074, -0.1278): "London",
}
print(locations[(40.7128, -74.0060)]) # New York
# Lists can't be dict keys!
# {[1, 2]: "value"} # ❌ TypeError: unhashable type: 'list'
# 🗂️ SECTION 7 — Dictionaries
# Creating & Basic Operations
python
# Creating
empty = {}
person = {"name": "Alice", "age": 30, "city": "NYC"}
from_pairs = dict([("a", 1), ("b", 2)])
from_kwargs = dict(name="Alice", age=30)
# Access
print(person["name"]) # "Alice"
print(person.get("name")) # "Alice"
print(person.get("phone")) # None (no KeyError!)
print(person.get("phone", "N/A")) # "N/A" (default value)
# Modify
person["age"] = 31 # update
person["email"] = "a@a.com" # add new key
del person["city"] # delete key
# Check key existence
print("name" in person) # True
print("phone" not in person) # True
# Dictionary Methods
python
d = {"a": 1, "b": 2, "c": 3}
# Views (live views — update when dict changes!)
print(d.keys()) # dict_keys(['a', 'b', 'c'])
print(d.values()) # dict_values([1, 2, 3])
print(d.items()) # dict_items([('a', 1), ('b', 2), ('c', 3)])
for key, value in d.items():
print(f"{key} = {value}")
# Update
d.update({"d": 4, "e": 5}) # add/update multiple
d.update(f=6) # keyword syntax
# Pop
val = d.pop("a") # remove & return — KeyError if missing
val = d.pop("z", 0) # with default — no error
last = d.popitem() # remove & return last item (3.7+)
# setdefault — get or set default
d.setdefault("x", 0) # adds "x":0 only if "x" not in d
print(d.setdefault("a", 99)) # returns existing value, doesn't overwrite
# fromkeys — create dict from keys
keys = ["a", "b", "c"]
d = dict.fromkeys(keys, 0) # {"a": 0, "b": 0, "c": 0}
d = dict.fromkeys(keys) # {"a": None, "b": None, "c": None}
# copy
shallow = d.copy()
# Shallow! Nested structures share reference (use copy.deepcopy for nested)
# clear
d.clear() # removes all items
# Dictionary Comprehension ⭐
python
# {key_expr: value_expr for item in iterable if condition}
squares = {x: x**2 for x in range(6)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
# {1: 'a', 2: 'b'}
filtered = {k: v for k, v in squares.items() if v > 5}
# {3: 9, 4: 16, 5: 25}
# Word frequency count
words = "the quick brown fox jumps over the lazy dog".split()
freq = {word: words.count(word) for word in set(words)}
# Merging Dicts
python
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
# Python 3.9+ — | operator ⭐
merged = d1 | d2 # {"a": 1, "b": 3, "c": 4} — d2 wins conflicts
d1 |= d2 # in-place merge
# Python 3.5+ — ** unpacking
merged = {**d1, **d2} # same result
# Old way
merged = {}
merged.update(d1)
merged.update(d2)
# `collections.defaultdict` ⭐
python
from collections import defaultdict
# Regular dict raises KeyError for missing keys
# defaultdict creates default value automatically!
# Group words by first letter
words = ["apple", "ant", "banana", "bear", "cherry"]
grouped = defaultdict(list)
for word in words:
grouped[word[0]].append(word)
# {'a': ['apple', 'ant'], 'b': ['banana', 'bear'], 'c': ['cherry']}
# Count occurrences
counts = defaultdict(int) # default value is int() = 0
for word in words:
counts[word] += 1
# Nested defaultdict
matrix = defaultdict(lambda: defaultdict(int))
matrix[0][0] = 1
matrix[0][1] = 2
# `collections.Counter` ⭐
python
from collections import Counter
words = "the the the quick brown fox fox".split()
c = Counter(words)
print(c) # Counter({'the': 3, 'fox': 2, 'quick': 1, ...})
print(c["the"]) # 3
print(c["unknown"]) # 0 (no KeyError!)
# Most common
print(c.most_common(2)) # [('the', 3), ('fox', 2)]
# Arithmetic on counters
c1 = Counter(a=3, b=2)
c2 = Counter(a=1, b=5)
print(c1 + c2) # Counter({'b': 7, 'a': 4})
print(c1 - c2) # Counter({'a': 2}) — negatives removed!
print(c1 & c2) # Counter({'a': 1, 'b': 2}) — min
print(c1 | c2) # Counter({'b': 5, 'a': 3}) — max
# Total count
print(sum(c.values())) # or c.total() in Python 3.10+
# Update
c.update(["the", "the"]) # adds counts
c.subtract(["the"]) # subtracts counts
# Nested Dicts
python
config = {
"database": {
"host": "localhost",
"port": 5432,
"credentials": {
"user": "admin",
"pass": "secret"
}
},
"server": {
"host": "0.0.0.0",
"port": 8000
}
}
# Access nested
db_host = config["database"]["host"]
db_user = config["database"]["credentials"]["user"]
# Safe nested access with .get()
port = config.get("database", {}).get("port", 3306)
# 🎯 SECTION 8 — Sets
# Creating Sets
python
# IMPORTANT: {} creates empty DICT, not set!
empty_set = set() # ✅ correct
empty_dict = {} # ← this is a dict!
s = {1, 2, 3, 4, 5}
from_list = set([1, 2, 2, 3, 3, 3]) # {1, 2, 3} — duplicates removed!
from_string = set("hello") # {'h', 'e', 'l', 'o'}
# Set Methods
python
s = {1, 2, 3}
# Add/Remove
s.add(4) # add single element — O(1)
s.remove(2) # remove — KeyError if not found
s.discard(99) # remove — NO error if not found ✅
s.pop() # remove & return arbitrary element (sets are unordered!)
s.clear() # empty the set
# Set Operations ⭐
A = {1, 2, 3, 4, 5}
B = {3, 4, 5, 6, 7}
# Union — all elements from both
print(A | B) # {1, 2, 3, 4, 5, 6, 7}
print(A.union(B)) # same
# Intersection — elements in BOTH
print(A & B) # {3, 4, 5}
print(A.intersection(B)) # same
# Difference — in A but NOT in B
print(A - B) # {1, 2}
print(A.difference(B)) # same
# Symmetric difference — in either but NOT both
print(A ^ B) # {1, 2, 6, 7}
print(A.symmetric_difference(B)) # same
# In-place operations
A |= B # update A with union
A &= B # keep only intersection
A -= B # remove B's elements from A
A ^= B # keep symmetric difference
# Subset/Superset
print({1, 2}.issubset({1, 2, 3})) # True
print({1, 2, 3}.issuperset({1, 2})) # True
print({1, 2}.isdisjoint({3, 4})) # True (no common elements)
# Set Comprehension
python
squares = {x**2 for x in range(10)} # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
even_squares = {x**2 for x in range(10) if x % 2 == 0}
# Remove duplicates from list (preserving order — Python 3.7+)
lst = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
unique = list(dict.fromkeys(lst)) # ✅ preserves order!
unique_set = list(set(lst)) # ❌ loses order!
# `frozenset` — Immutable Set
python
fs = frozenset({1, 2, 3})
# Can be used as dict key (unlike regular set!)
d = {frozenset({1, 2}): "value"}
# Can be set element
s = {frozenset({1, 2}), frozenset({3, 4})}
# Has same operations as set (except in-place operations)
print(fs | frozenset({4, 5})) # new frozenset {1, 2, 3, 4, 5}
# Set for O(1) Lookups ⭐
python
# List lookup = O(n) — checks each element
# Set lookup = O(1) — hash table!
# Performance comparison
large_list = list(range(1_000_000))
large_set = set(range(1_000_000))
import timeit
# Checking membership
print(timeit.timeit("999999 in large_list", globals=globals(), number=100))
# ~2.5 seconds (O(n))
print(timeit.timeit("999999 in large_set", globals=globals(), number=100))
# ~0.000001 seconds (O(1)) — MASSIVE difference!
# Practical use: valid items check
VALID_COMMANDS = {"quit", "help", "list", "add", "remove"}
user_input = "quit"
if user_input in VALID_COMMANDS: # O(1) lookup!
print("Valid command")
# Remove duplicates
emails = ["a@a.com", "b@b.com", "a@a.com", "c@c.com"]
unique_emails = list(set(emails))
# 🏆 Extra Gyan By Your Bhai
# 🔥 Most Common Gotchas in Sections 1-8
python
# 1. Mutable default argument (BIGGEST GOTCHA!)
def bad(x=[]): # ❌ THE CLASSIC MISTAKE
x.append(1)
return x
# bad() = [1], bad() = [1,1], bad() = [1,1,1]...
# 2. Integer caching
a = 256; b = 256
print(a is b) # True (Python caches -5 to 256!)
a = 257; b = 257
print(a is b) # False (no caching!)
# 3. String interning
a = "hello"; b = "hello"
print(a is b) # True (interned — short strings)
a = "hello world"; b = "hello world"
print(a is b) # maybe False (depends on implementation)
# 4. Empty dict vs empty set
x = {} # DICT! Not set!
y = set() # correct empty set
# 5. List multiplication with mutables
matrix = [[0] * 3] * 3 # ❌ all rows are SAME object!
matrix[0][0] = 1
print(matrix) # [[1,0,0],[1,0,0],[1,0,0]] SHOCK!
matrix = [[0]*3 for _ in range(3)] # ✅ separate lists
matrix[0][0] = 1
print(matrix) # [[1,0,0],[0,0,0],[0,0,0]] ✅
# 6. Modifying list while iterating
lst = [1, 2, 3, 4, 5]
for x in lst: # ❌ modifying while iterating = undefined behavior!
if x == 2:
lst.remove(x)
# ✅ iterate over copy or use comprehension
lst = [x for x in lst if x != 2]
# 7. Dictionary key order (Python 3.7+ insertion ordered)
d = {"b": 2, "a": 1, "c": 3}
print(list(d.keys())) # ['b', 'a', 'c'] — insertion order preserved!
Part 1 Complete — Sections 1-8 ✅
Next: Part 2 — OOP, Decorators, Generators, Context Managers, Exceptions, File I/O, Numbers, DateTime