python/python-part-1

Python Part 1

# 🐍 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