V1
Back to handbooks index
Py
Python 101
3.12 · 2025
For C# / Java / PHP devs Basics → Frameworks ML · Web · Data
101 Handbook

Python for the
Seasoned Developer

You know C#, Java, or PHP. Now let's learn Python.

A focused guide for developers who already think in objects, types, and abstractions. Skip the "what is a variable" basics — dive straight into what makes Python different, what to unlearn, and how to be productive from day one.

Python 3.12 vs C# / Java / PHP Syntax Gotchas Decorators & Magic All Major Frameworks
Introduction

Why Python?

Python has grown from a scripting language into the dominant force in machine learning, data science, and automation — while remaining excellent for web APIs. If you're coming from C#, Java, or PHP, you're not switching to Python because it's "better" — you're switching because the ecosystem for your target domain lives here.

#1
TIOBE Index 2025
30+
Years Old
500K+
PyPI Packages
CPython
Reference Impl.
GIL
Threading Model
🤖
ML / AI Dominance
TensorFlow, PyTorch, scikit-learn, Hugging Face — every major ML library is Python-first. No Python = no AI ecosystem.
📊
Data Science
pandas, NumPy, Jupyter, Matplotlib — the entire data analysis workflow is native Python. No competition here.
🌐
Web APIs
FastAPI, Django, Flask — excellent async web frameworks. Not as fast as Go/Rust but developer velocity is unmatched.
⚙️
Automation & DevOps
Ansible (Python-based), scripting, CI/CD tools, Kubernetes operators — Python is the glue language of infrastructure.
Introduction

The Zen of Python

Type import this in any Python interpreter and you get 19 guiding aphorisms written by Tim Peters. These aren't just philosophy — they explain why Python code looks and feels the way it does, and why Pythonistas push back on "clever" code.

import this — PEP 20
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.
Errors should never pass silently.
In the face of ambiguity, refuse the temptation to guess.
There should be one — and preferably only one — obvious way to do it.
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.
Namespaces are one honking great idea — let's do more of those!
💡 What this means for C#/Java/PHP devs

Python resists over-engineering. No AbstractFactoryBeanFactory. No 5-level class hierarchies for simple tasks. If you find yourself writing very clever, dense code — stop. Python favors the readable solution over the "efficient" one. Code is read far more often than it is written.

Introduction

Python vs C# / Java / PHP

Here are the most important mental shifts. These aren't just syntax differences — they're philosophical ones that will trip you up if you try to code Python like Java.

FeatureC# / Java / PHPPython
TypingStatic (declared types)Dynamic (types at runtime) + optional type hints
Blocks{ } bracesIndentation (mandatory, 4 spaces)
Statement end; semicolonsNewline (no semicolons needed)
Entry pointMain() methodif __name__ == "__main__":
Nullnull / NULLNone
Booleantrue / falseTrue / False (capitalised!)
ArrayFixed-type array / ArrayListlist — mixed types, dynamic size
HashMap / DictionaryHashMap / Dictionary<K,V>dict — built-in, first-class
String formatting$"{var}" / String.format()f"{var}" f-strings (same idea, cleaner)
Package managerNuGet / Maven / Composerpip / Poetry / uv
Access modifierspublic / private / protectedConvention: _private / __dunder
Interfacesinterface keywordABC (Abstract Base Classes) or duck typing
GenericsList<T>Type hints: list[int] (3.9+)
Switch/caseswitch statementmatch (Python 3.10+) — much more powerful
Ternarycondition ? a : ba if condition else b
Incrementi++ / i--No ++ / -- — use i += 1
CompilationCompiled to bytecode / binaryInterpreted (CPython) or compiled (.pyc bytecode)
GCManaged GCReference counting + cyclic GC
ConcurrencyThreads + async/awaitGIL limits true threads; use asyncio or multiprocessing

The 5 Things That Will Bite You Immediately

1. Indentation IS the syntax

A tab where there should be 4 spaces (or vice versa) is a SyntaxError. Use an editor with Python support. Configure your editor to use 4 spaces, not tabs.

2. No ++ operator

There is no i++ in Python. Write i += 1. This surprises every Java/C# developer exactly once.

3. True and False are capitalized

true is a NameError. True is a boolean. Same for None (not null). Memorize it once, never again.

4. Mutable default arguments

def func(items=[]): — this list is created ONCE and shared across all calls. Use None as default, then assign inside.

5. Everything is a reference

a = [1,2,3]; b = ab is NOT a copy. Both a and b point to the same list. Use b = a.copy() or b = a[:].

6. Duck Typing

"If it walks like a duck and quacks like a duck, it's a duck." Python doesn't check types at compile time — if an object has the right methods, it works. No interfaces required.

Setup

Installation & Tools

bashSetup via pyenv (recommended)
# Install pyenv (manages multiple Python versions — like nvm for Node)
curl https://pyenv.run | bash

# Install Python 3.12
pyenv install 3.12.3
pyenv global 3.12.3    # Set as default system version
pyenv local 3.12.3     # Set for current directory only

# Verify
python --version        # Python 3.12.3
python3 --version       # Same on macOS/Linux (use python3 to be safe)

# REPL — interactive Python shell
python
>>> print("Hello, Python!")
>>> 2 + 2
4
>>> exit()
📌 python vs python3

On macOS/Linux, python may point to Python 2 (ancient, deprecated). Always check with python --version. Use python3 explicitly if unsure. pyenv solves this cleanly by controlling which binary python refers to.

Setup

Virtual Environments

Python doesn't have a project-level package scope like NuGet (per-solution) or Maven (per-project). Instead you use virtual environments — isolated Python installations per project. Always create one. Always.

bashvenv (built-in) + activation
# Create virtual environment in .venv folder
python -m venv .venv

# Activate
source .venv/bin/activate          # macOS / Linux
.venv\Scripts\activate             # Windows PowerShell
.venv\Scripts\activate.bat         # Windows CMD

# Verify — should show (.venv) in prompt
which python                        # .venv/bin/python

# Deactivate
deactivate

# Add to .gitignore!
echo ".venv/" >> .gitignore
Setup

pip & Poetry

pip — built-in package manager
# Install a package
pip install fastapi

# Install specific version
pip install fastapi==0.110.0

# Install from requirements file
pip install -r requirements.txt

# Save current packages
pip freeze > requirements.txt

# Uninstall
pip uninstall fastapi

# List installed
pip list
pip show fastapi
Poetry — modern dependency management
# Install Poetry
pip install poetry

# Create new project
poetry new my-project
poetry init      # In existing folder

# Add dependency
poetry add fastapi
poetry add pytest --group dev

# Install all deps
poetry install

# Run script in env
poetry run python main.py

# pyproject.toml = package.json / .csproj

Poetry is to Python what NuGet is to .NET or Maven to Java — a proper dependency + packaging tool. pyproject.toml is the modern equivalent of .csproj or pom.xml. Use Poetry for new projects, pip for quick scripts.

Syntax Fundamentals

Indentation & No Braces

This is the single biggest shock for C#/Java developers. Python uses indentation to define code blocks — no curly braces, no begin/end. The colon : at the end of a statement starts a new block, and the block ends when indentation returns.

Java / C# (braces)
if (score > 90) {
    System.out.println("A");
    System.out.println("Excellent");
} else if (score > 70) {
    System.out.println("B");
} else {
    System.out.println("Try harder");
}

for (int i = 0; i < 5; i++) {
    System.out.println(i);
}
Python (indentation)
if score > 90:
    print("A")
    print("Excellent")
elif score > 70:
    print("B")
else:
    print("Try harder")

for i in range(5):
    print(i)
# No braces. No semicolons. Just indentation.
⚡ Rules

Use 4 spaces per level (PEP 8 standard — like K&R style). Never mix tabs and spaces in the same file — Python 3 raises a TabError. Configure your editor to convert tabs to spaces automatically. VS Code, PyCharm, and Vim all have this setting.

Syntax Fundamentals

Variables & Dynamic Typing

pythonVariables — no type declarations needed
# No type declaration — Python infers the type
name = "Alice"         # str
age  = 30             # int
pi   = 3.14159        # float
active = True         # bool (capital T!)
nothing = None        # NoneType (like null)

# Types can change (dynamic typing)
x = 42
x = "now I'm a string"   # perfectly valid Python
x = [1, 2, 3]          # now a list — no errors

# Multiple assignment
a, b, c = 1, 2, 3
first, *rest = [1, 2, 3, 4]   # first=1, rest=[2,3,4]
x, y = y, x               # swap without temp variable!

# Constants — convention only (no const keyword)
MAX_RETRIES = 3         # ALL_CAPS = "please don't change me"
PI = 3.14159

# Type annotations (optional, but recommended)
username: str = "alice"
count: int = 0
scores: list[int] = [95, 87, 92]
Data Types Overview

Built-in Data Types

TypeExampleMutable?C# / Java Equivalent
int42, -10, 10_000_000Noint / long (unlimited size!)
float3.14, 1.5e10Nodouble
complex3+4jNoNo direct equivalent
boolTrue, FalseNobool (subclass of int!)
str"hello", 'world'Nostring
bytesb"hello"Nobyte[]
list[1, "a", True]YesList<object> / ArrayList
tuple(1, 2, 3)NoValueTuple / record
dict{"key": "val"}YesDictionary<K,V> / HashMap
set{1, 2, 3}YesHashSet<T>
frozensetfrozenset({1,2})NoImmutable HashSet
NoneTypeNonenull
🔑 Python int is unlimited precision

In Java/C#, int overflows at 2,147,483,647. In Python, int can be any size — 2 ** 1000 is valid. No long, no BigInteger needed. Python handles it automatically.

Data Types In Depth

Strings & f-strings

pythonString creation and f-strings
# All four quoting styles are equivalent
s1 = 'single quotes'
s2 = "double quotes"
s3 = """triple double — multiline
spans multiple lines
no backslash needed"""
s4 = '''triple single — same thing'''

# f-strings (Python 3.6+) — like C# $"" interpolation
name = "Alice"
age  = 30
greeting = f"Hello, {name}! You are {age} years old."

# Expressions inside f-strings
result = f"2 + 2 = {2 + 2}"
upper  = f"{name.upper()}"
price  = f"Price: {99.9:.2f}"        # format spec
padded = f"{'centered':^20}"          # centering
debug  = f"{name=}"                   # Python 3.8+: prints name='Alice'

# String methods (strings are immutable — these return NEW strings)
s = "  Hello, World!  "
s.strip()            # "Hello, World!"  — like Trim()
s.lower()            # "  hello, world!  "
s.upper()            # "  HELLO, WORLD!  "
s.replace("World", "Python")
s.split(",")         # ["  Hello", " World!  "]
s.startswith("  H")  # True
",".join(["a", "b", "c"])  # "a,b,c"  — like String.Join()

# Raw strings (no escape processing) — useful for regex, paths
path  = r"C:\Users\Alice\Documents"
regex = r"\d{3}-\d{4}"

# String slicing — Python-exclusive power feature
s = "Hello, World!"
s[0]       # 'H'       — 0-indexed
s[-1]      # '!'       — negative = from end
s[0:5]     # 'Hello'   — slice [start:end]
s[7:]      # 'World!'  — from index 7 to end
s[::-1]    # '!dlroW ,olleH' — reversed!
Data Types In Depth

Numbers & Booleans

pythonArithmetic + Boolean logic
# Integer division — KEY DIFFERENCE from Java/C#
7 / 2      # 3.5  — always returns float (C#: 3)
7 // 2     # 3    — integer (floor) division
7 % 2      # 1    — modulo
2 ** 10    # 1024 — power (no Math.Pow() needed)

# Underscores in numbers (readability)
million = 1_000_000
pi = 3.141_592_653

# Boolean is a subtype of int!
int(True)   # 1
int(False)  # 0
True + True  # 2  (this is valid Python!)

# Logical operators — WORDS, not symbols!
x = True and False   # False  (not &&)
y = True or False    # True   (not ||)
z = not True          # False  (not !)

# Comparison chaining — unique to Python!
0 < x < 10       # True if x is between 0 and 10 — no && needed
1 == 1 == 1     # True

# Truthiness — these are all falsy in Python:
# None, False, 0, 0.0, "", [], {}, set(), ()
# Everything else is truthy

if my_list:       # Pythonic way to check if list is not empty
    print("has items")
if not my_string: # Check if string is empty or None
    print("empty")
Data Types In Depth

Lists (Python's Swiss Army Knife)

Python's list is ordered, mutable, allows mixed types, and is resizable. It's used where you'd use List<T>, ArrayList, or PHP arrays in other languages.

pythonLists — creation, mutation, slicing
# Creation
nums    = [1, 2, 3, 4, 5]
mixed   = [1, "hello", True, None]   # mixed types — valid!
empty   = []
matrix  = [[1, 2], [3, 4]]            # nested lists

# Access + Slicing
nums[0]          # 1   — first element
nums[-1]         # 5   — last element
nums[1:3]        # [2, 3]
nums[::2]        # [1, 3, 5]  — every 2nd element
nums[::-1]       # [5, 4, 3, 2, 1]  — reversed

# Mutation
nums.append(6)          # Add to end — like List.Add()
nums.insert(0, 0)       # Insert at index 0
nums.extend([7, 8])     # Append multiple — like AddRange()
nums.remove(3)          # Remove first occurrence of value 3
popped = nums.pop()     # Remove + return last element
popped = nums.pop(0)   # Remove + return at index 0
nums.sort()             # In-place sort
nums.reverse()          # In-place reverse
nums.clear()            # Empty the list

# Non-mutating operations
sorted_copy = sorted(nums)    # Returns new sorted list
length = len(nums)             # Length — like .Count or .length
total  = sum(nums)             # Sum all numbers
best   = max(nums)
worst  = min(nums)

# Membership test
3 in nums             # True — like .Contains()
99 not in nums         # True

# List concatenation
combined = [1, 2] + [3, 4]   # [1, 2, 3, 4]
repeated = [0] * 5           # [0, 0, 0, 0, 0]
Data Types In Depth

Tuples — Immutable Records

pythonTuples
# Tuples are immutable lists — use for data that shouldn't change
point    = (10, 20)
rgb      = (255, 128, 0)
single   = (42,)         # Single element — NOTE the trailing comma!
empty    = ()

# Unpacking (very common Python pattern)
x, y = point              # x=10, y=20
r, g, b = rgb

# Functions can return multiple values via tuple
def divmod_custom(a, b):
    return a // b, a % b   # Returns a tuple

quotient, remainder = divmod_custom(17, 5)

# Named tuples — like simple data classes
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
p.x  # 3  — access by name
p[0] # 3  — also by index
Data Types In Depth

Dictionaries — The Heart of Python

Dictionaries are Python's most important data structure. They are ordered (insertion order preserved since 3.7), mutable, and incredibly fast for lookup. They're everywhere — JSON responses, config, keyword arguments, object internals.

pythonDictionary operations
# Creation
person = {
    "name": "Alice",
    "age": 30,
    "skills": ["Python", "K8s"]
}

empty_dict = {}
from_keys  = dict(name="Bob", age=25)    # Alternative syntax

# Access
person["name"]                   # "Alice"  — raises KeyError if missing
person.get("email")              # None     — safe, no exception
person.get("email", "unknown")  # "unknown" — with default

# Mutation
person["email"] = "alice@example.com"    # Add or update
person.update({"city": "London", "age": 31})  # Merge dicts
del person["email"]                        # Delete key
popped = person.pop("age")                # Remove + return

# Iteration
for key in person:               # Iterate keys
    print(key)

for key, value in person.items(): # Key-value pairs — most common
    print(f"{key}: {value}")

for value in person.values():    # Values only
    print(value)

# Membership
"name" in person       # True — checks keys

# Dict merging (Python 3.9+)
defaults = {"theme": "dark", "lang": "en"}
user_prefs = {"theme": "light"}
merged = defaults | user_prefs    # {"theme": "light", "lang": "en"}

# Dict comprehension
squares = {x: x**2 for x in range(5)}   # {0:0, 1:1, 2:4, 3:9, 4:16}
Data Types In Depth

Sets — Unique Unordered Collections

pythonSets and set operations
# Sets — unique elements, unordered, O(1) membership test
fruits = {"apple", "banana", "apple"}  # {"apple", "banana"} — deduped
empty_set = set()                       # NOT {} — that creates empty dict!

# Convert list to set to deduplicate
dupes = [1, 2, 2, 3, 3, 3]
unique = list(set(dupes))   # [1, 2, 3] — order not guaranteed

# Set operations (like HashSet in C#/Java)
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b          # {1,2,3,4,5,6}  — Union
a & b          # {3, 4}          — Intersection
a - b          # {1, 2}          — Difference (in a, not in b)
a ^ b          # {1,2,5,6}       — Symmetric difference

3 in a         # True  — O(1) lookup (use set not list for fast membership)
Control Flow

if / elif / else

pythonConditionals
# Standard if/elif/else
score = 85
if score >= 90:
    grade = "A"
elif score >= 80:           # elif — not "else if"
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

# Ternary — Python style (condition last!)
grade = "Pass" if score >= 60 else "Fail"

# Identity vs equality
x = [1, 2, 3]
y = [1, 2, 3]
z = x

x == y       # True  — same VALUE
x is y       # False — different OBJECTS (like ReferenceEquals)
x is z       # True  — same object
x is None    # ALWAYS use 'is None', not '== None'
Control Flow

Loops & Iterators

pythonfor, while, enumerate, zip, range
# for-each — iterates over any iterable
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

# range — equivalent of for(int i=0; i<10; i++)
for i in range(10):       # 0..9
    print(i)
for i in range(2, 10, 2): # 2, 4, 6, 8
    print(i)

# enumerate — get index AND value (no i++ needed)
for index, fruit in enumerate(fruits):
    print(f"{index}: {fruit}")
for index, fruit in enumerate(fruits, start=1):  # Start from 1
    print(f"{index}: {fruit}")

# zip — iterate multiple lists in parallel
names  = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# while loop
count = 0
while count < 5:
    print(count)
    count += 1              # No ++ in Python!

# break, continue, else-on-loop (unique to Python!)
for i in range(10):
    if i == 5: break
    if i % 2 == 0: continue
    print(i)
else:
    print("loop completed without break")  # Runs if no break!
Control Flow

Comprehensions — Pythonic Data Transforms

List comprehensions are one of Python's most celebrated features. They replace verbose for loops with readable one-liners. Think LINQ's .Select() and .Where(), but built into the language syntax.

pythonList, dict, set, and generator comprehensions
# List comprehension: [expression for item in iterable if condition]
nums = [1, 2, 3, 4, 5, 6]

squares      = [x**2 for x in nums]          # [1,4,9,16,25,36]
evens        = [x for x in nums if x % 2 == 0] # [2,4,6]
even_squares = [x**2 for x in nums if x % 2 == 0]  # [4,16,36]

# Equivalent verbose for loop (don't write this in Python!):
# result = []
# for x in nums:
#     if x % 2 == 0:
#         result.append(x**2)

# Dict comprehension
word_lengths = {word: len(word) for word in ["cat", "elephant", "ox"]}
# {"cat": 3, "elephant": 8, "ox": 2}

# Set comprehension
unique_lengths = {len(word) for word in ["cat", "rat", "elephant"]}
# {3, 8}  — set deduplicates

# Generator expression — lazy (doesn't create list in memory)
total = sum(x**2 for x in range(1_000_000))  # No big list created
Control Flow

match — Structural Pattern Matching

pythonmatch/case (Python 3.10+) — far more powerful than Java switch
# Basic match — like switch/case
command = "quit"
match command:
    case "quit" | "exit":
        print("Goodbye")
    case "help":
        print("Available commands...")
    case _:               # Default case
        print("Unknown command")

# Pattern matching with data structures
point = (0, 5)
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"On Y-axis at {y}")
    case (x, 0):
        print(f"On X-axis at {x}")
    case (x, y):
        print(f"Point at ({x}, {y})")

# Match on object type (guard condition)
match response.status_code:
    case code if 200 <= code < 300:
        print("Success")
    case 404:
        print("Not found")
    case 500 | 502 | 503:
        print("Server error")
Functions

Functions Basics

In Python, functions are first-class citizens — they can be stored in variables, passed as arguments, returned from other functions. No static keyword needed. Default arguments, keyword arguments, and positional-only arguments give Python functions tremendous flexibility.

pythonFunction definition and features
# Basic function
def greet(name: str) -> str:
    """Docstring — Python's equivalent of XML doc comments.
    Explains what the function does. Access via help(greet).
    """
    return f"Hello, {name}!"

# Default arguments
def connect(host: str, port: int = 8080, ssl: bool = False):
    print(f"Connecting to {host}:{port} SSL={ssl}")

connect("localhost")              # uses defaults
connect("prod.server", ssl=True)  # keyword argument (order doesn't matter)
connect("prod.server", 443, True) # positional

# ⚠️ GOTCHA: NEVER use mutable as default!
def bad(items=[]):      # ❌ same list reused across calls!
    items.append(1)
    return items

def good(items=None):  # ✅ create new list each call
    if items is None:
        items = []
    items.append(1)
    return items

# Multiple return values (returns a tuple)
def minmax(numbers: list[int]) -> tuple[int, int]:
    return min(numbers), max(numbers)

low, high = minmax([3, 1, 4, 1, 5])   # tuple unpacking

# Functions as first-class values
def apply(func, value):
    return func(value)

apply(str.upper, "hello")    # "HELLO" — passing function by name
apply(abs, -42)              # 42
Functions

*args & **kwargs

pythonVariable arguments — equivalent to params[] but more powerful
# *args — variable positional arguments (like params[] in C#)
def total(*args: int) -> int:
    """args is a tuple of all positional args"""
    return sum(args)

total(1, 2, 3)          # 6
total(1, 2, 3, 4, 5)    # 15

# **kwargs — variable keyword arguments (dict of named args)
def create_user(**kwargs):
    """kwargs is a dict: {"name": "Alice", "age": 30}"""
    for key, value in kwargs.items():
        print(f"{key} = {value}")

create_user(name="Alice", age=30, city="London")

# Combining all types
def full_func(required, *args, keyword_only=False, **kwargs):
    print(required, args, keyword_only, kwargs)

# Unpacking into function call
nums = [1, 2, 3]
options = {"sep": ",", "end": "\n"}
print(*nums)              # Unpacks list as positional args
print("a", **options)     # Unpacks dict as keyword args
Functions

Lambdas & Closures

pythonlambda expressions and closures
# Lambda — anonymous function, single expression only
# lambda arguments: expression

double = lambda x: x * 2        # like x => x * 2 in C#
add    = lambda x, y: x + y

# Lambdas shine as inline sort keys
people = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
people.sort(key=lambda p: p[1])     # Sort by age

words = ["banana", "apple", "cherry"]
sorted(words, key=lambda w: w[-1])  # Sort by last character

# With map() and filter() (though comprehensions are preferred)
squared = list(map(lambda x: x**2, [1,2,3]))   # [1,4,9]
evens   = list(filter(lambda x: x%2==0, [1,2,3,4])) # [2,4]

# Closure — function that captures outer scope variables
def make_multiplier(factor):
    def multiply(x):
        return x * factor     # captures 'factor' from outer scope
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
double(5)  # 10
triple(5)  # 15
Functions

Decorators — Python's AOP

Decorators are Python's way of wrapping a function with additional behaviour without modifying it. Think AOP (Aspect-Oriented Programming), C# attributes that actually execute, or Java annotations that do something at runtime. They're used heavily throughout the ecosystem — Flask routes, FastAPI endpoints, pytest fixtures, dataclasses, property accessors.

pythonDecorators — from scratch to real-world
# A decorator is just a function that takes a function and returns a function
def timer(func):
    import time
    from functools import wraps

    @wraps(func)              # Preserves original function metadata
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

# Apply with @ syntax
@timer
def slow_function():
    import time
    time.sleep(0.1)

slow_function()   # Prints: slow_function took 0.1001s
# This is equivalent to: slow_function = timer(slow_function)

# Decorator with arguments
def retry(max_attempts=3, delay=1.0):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1: raise
                    print(f"Attempt {attempt+1} failed: {e}")
        return wrapper
    return decorator

@retry(max_attempts=5, delay=2.0)
def fetch_data(url: str): ...

# Built-in decorators you'll use constantly
class Circle:
    @property                          # Getter property — like C# get
    def area(self) -> float:
        return 3.14159 * self.radius**2

    @staticmethod                      # Like C# static method
    def unit_circle() -> "Circle":
        return Circle(radius=1)

    @classmethod                       # Factory method — receives class
    def from_diameter(cls, d) -> "Circle":
        return cls(radius=d/2)
OOP

Classes & Objects

pythonPython OOP — a complete class example
class BankAccount:
    """A simple bank account class."""

    # Class variable — shared by all instances (like static field)
    interest_rate: float = 0.05
    _all_accounts: list = []

    # Constructor — __init__ is Python's constructor (note: NOT __new__)
    def __init__(self, owner: str, balance: float = 0.0):
        self.owner = owner         # public instance attribute
        self._balance = balance    # _prefix = "please treat as private"
        self.__id = id(self)      # __prefix = name-mangled (harder to access)
        BankAccount._all_accounts.append(self)

    # Property — like C# { get; set; }
    @property
    def balance(self) -> float:
        return self._balance

    @balance.setter
    def balance(self, amount: float):
        if amount < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = amount

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def withdraw(self, amount: float) -> bool:
        if amount > self._balance:
            return False
        self._balance -= amount
        return True

    # __str__ = ToString()  — called by str() and print()
    def __str__(self) -> str:
        return f"Account({self.owner}: £{self._balance:.2f})"

    # __repr__ — developer representation (for debugging)
    def __repr__(self) -> str:
        return f"BankAccount(owner={self.owner!r}, balance={self._balance})"

    # Static method
    @staticmethod
    def validate_amount(amount: float) -> bool:
        return amount > 0

    # Class method — factory pattern
    @classmethod
    def get_all_accounts(cls) -> list:
        return cls._all_accounts

# Usage
acc = BankAccount("Alice", 1000)
acc.deposit(500)
print(acc)             # Account(Alice: £1500.00)
acc.balance = 2000    # Calls setter
OOP

Inheritance & ABCs

pythonInheritance + Abstract Base Classes (interface equivalent)
from abc import ABC, abstractmethod

# Abstract Base Class — Python's "interface"
class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

    def describe(self):      # Concrete method in ABC (like default interface methods)
        print(f"Area={self.area():.2f}, Perimeter={self.perimeter():.2f}")

# Inheritance — class Child(Parent):
class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius**2

    def perimeter(self) -> float:
        return 2 * 3.14159 * self.radius

class Square(Shape):
    def __init__(self, side: float):
        self.side = side

    def area(self) -> float: return self.side**2
    def perimeter(self) -> float: return 4 * self.side

# super() — like base. in C# or super() in Java
class SavingsAccount(BankAccount):
    def __init__(self, owner, balance=0, interest_rate=0.05):
        super().__init__(owner, balance)   # Call parent constructor
        self.interest_rate = interest_rate

# Multiple inheritance (Python supports it — use with care)
class PremiumSavings(SavingsAccount, Mixin): ...

# Type checking
c = Circle(5)
isinstance(c, Circle)    # True — like 'is' in C#/Java
isinstance(c, Shape)     # True — checks inheritance chain
issubclass(Circle, Shape) # True
OOP

Dataclasses — Goodbye Boilerplate

Dataclasses auto-generate __init__, __repr__, __eq__ and more from field declarations. Similar to C# records, Kotlin data classes, or Java records.

pythondataclass vs manual class
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class Product:
    name: str
    price: float
    category: str = "general"         # Default value
    tags: list[str] = field(default_factory=list)  # Mutable default!
    created_at: datetime = field(default_factory=datetime.now)

# Auto-generated: __init__, __repr__, __eq__
p = Product("Laptop", 999.99)
print(p)  # Product(name='Laptop', price=999.99, ...)

# Immutable dataclass (like record in C# 9)
@dataclass(frozen=True)
class Point:
    x: float
    y: float

# Pydantic (even better for APIs — includes validation)
from pydantic import BaseModel, validator

class UserCreate(BaseModel):
    username: str
    email: str
    age: int

    @validator("age")
    def age_must_be_positive(cls, v):
        if v <= 0: raise ValueError("Age must be positive")
        return v
Python Peculiarities

__main__ — Python's Entry Point

This is one of the most confusing things for Java/C# developers. Python has no required Main() method. When Python runs a file, everything at the top level executes immediately. The if __name__ == "__main__": guard prevents code from running when the file is imported as a module.

Java — explicit entry point
public class Main {
    public static void main(String[] args) {
        // Only runs when executed directly
        System.out.println("Hello");
    }
}

// JVM always calls main() first
// Importing doesn't run main()
Python — __name__ guard
# math_utils.py
def add(a, b):     # This is always defined
    return a + b

# __name__ == "math_utils" if imported
# __name__ == "__main__"  if run directly
if __name__ == "__main__":
    # Only runs when: python math_utils.py
    # NOT when: import math_utils
    print(add(2, 3))
pythonIdiomatic main() pattern
# Best practice: wrap app logic in a main() function
# This makes testing easy and keeps top-level clean
import sys
import argparse

def process_data(filepath: str) -> None:
    print(f"Processing {filepath}")

def main() -> int:
    """Main entry point. Returns exit code."""
    parser = argparse.ArgumentParser(description="Data processor")
    parser.add_argument("filepath", help="Path to data file")
    parser.add_argument("--verbose", action="store_true")
    args = parser.parse_args()

    try:
        process_data(args.filepath)
        return 0
    except FileNotFoundError as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    sys.exit(main())
Python Peculiarities

Modules & Packages

pythonImport patterns
# Import module
import os
import json
import math

# Use with module prefix
os.path.join("home", "user")
math.sqrt(16)

# Import specific names
from os.path import join, exists
from datetime import datetime, timedelta

# Import with alias (common for long names)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Package structure:
# myapp/
#   __init__.py    ← marks directory as package
#   models.py
#   services/
#     __init__.py
#     user_service.py

# Relative imports (within a package)
from .models import User          # Same package
from ..utils import format_date    # Parent package

# Conditional import (for optional deps)
try:
    import ujson as json    # Fast JSON if available
except ImportError:
    import json              # Fall back to stdlib
🔑 Standard Library is Huge

Python's stdlib is famously "batteries included." Before reaching for a package: os, pathlib (paths), json, csv, re (regex), datetime, collections, itertools, functools, dataclasses, typing, asyncio, subprocess, logging, unittest — all built-in, no install needed.

Python Peculiarities

Error Handling

pythontry/except/else/finally + raising exceptions
# try/except — like try/catch
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
except (ValueError, TypeError) as e:   # Multiple exception types
    print(f"Type error: {e}")
except Exception as e:                 # Catch-all (base class)
    print(f"Unexpected error: {type(e).__name__}: {e}")
else:                                   # Runs if NO exception (unique to Python!)
    print(f"Result: {result}")
finally:                                # Always runs — like C# finally
    print("Cleanup")

# Raise exception
raise ValueError("Age must be positive")
raise RuntimeError("Something went wrong") from original_exception

# Custom exceptions
class InsufficientFundsError(Exception):
    def __init__(self, amount: float, balance: float):
        self.amount = amount
        self.balance = balance
        super().__init__(
            f"Cannot withdraw £{amount:.2f} from balance £{balance:.2f}")

# Common exception hierarchy
# BaseException
# └── Exception
#     ├── ValueError     — wrong value (like ArgumentException)
#     ├── TypeError      — wrong type
#     ├── KeyError       — dict key not found
#     ├── IndexError     — list index out of range
#     ├── AttributeError — object has no such attribute
#     ├── FileNotFoundError
#     ├── PermissionError
#     ├── TimeoutError
#     └── RuntimeError   — general runtime error
Python Peculiarities

Type Hints — Optional but Essential

Python is dynamically typed, but type hints (PEP 484+) let you annotate types for IDE support, static analysis (mypy), and documentation. They're not enforced at runtime by default — but they're strongly recommended for any codebase beyond scripts.

pythonType hints — 3.9+ modern syntax
from typing import Optional, Union, Any, Callable, TypeVar, Generic

# Basic annotations
def greet(name: str, age: int) -> str: ...
def process(data: bytes) -> None: ...

# Optional — value OR None (like Nullable<T> or T? in C#)
def find_user(id: int) -> Optional[str]: ...     # Old way
def find_user(id: int) -> str | None: ...         # Python 3.10+ new way

# Union — multiple types
def process(value: int | str | float) -> str: ...

# Collection types (3.9+: no need to import from typing)
def summarise(
    items:  list[str],
    lookup: dict[str, int],
    coords: tuple[float, float],
    unique: set[str]
) -> None: ...

# Callable type hint
def run(callback: Callable[[int, str], bool]) -> None: ...

# TypeVar — generics
T = TypeVar("T")
def first(items: list[T]) -> T | None:
    return items[0] if items else None

# Run mypy to check types:
# pip install mypy
# mypy main.py
Python Peculiarities

Context Managers — with statement

The with statement is Python's using (C#) or try-with-resources (Java). It ensures resources are properly acquired and released — file handles, database connections, locks, HTTP sessions.

pythonwith statement — the Pythonic resource pattern
# File handling — auto-closes even if exception occurs
with open("data.txt", "r") as f:
    content = f.read()
# File is closed here automatically

# Multiple context managers in one with
with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

# Common context managers
import threading
with threading.Lock():             # Acquire + release lock
    # Thread-safe code here
    pass

# Creating your own context manager
from contextlib import contextmanager

@contextmanager
def db_transaction(session):
    try:
        yield session          # Control passes to 'with' body
        session.commit()
    except Exception:
        session.rollback()
        raise

with db_transaction(db_session) as session:
    session.add(new_user)
Python Peculiarities

Generators — Lazy Sequences

pythonyield — memory-efficient iteration
# Generator function — uses yield instead of return
# Values are produced lazily — perfect for large datasets
def count_up(start: int, stop: int):
    while start <= stop:
        yield start           # Pause here, return value, resume on next()
        start += 1

for n in count_up(1, 1_000_000):  # Only one number in memory at a time!
    print(n)

# Read a huge CSV file line by line without loading it all
def read_large_file(filepath: str):
    with open(filepath) as f:
        for line in f:
            yield line.strip()

# yield from — delegate to another generator
def chain(*iterables):
    for it in iterables:
        yield from it

list(chain([1,2], [3,4], [5]))  # [1,2,3,4,5]
Python Peculiarities

async / await

Python's async/await looks identical to C#'s — the concepts transfer directly. The key difference: Python has the GIL (Global Interpreter Lock), so threads can't truly run in parallel. asyncio works around this with cooperative multitasking — great for I/O-bound tasks (APIs, databases), not CPU-bound work.

pythonasyncio fundamentals
import asyncio
import aiohttp   # pip install aiohttp — async HTTP client

# Async function — like Task<T> returning method in C#
async def fetch_user(user_id: int) -> dict:
    async with aiohttp.ClientSession() as session:
        async with session.get(f"https://api.example.com/users/{user_id}") as resp:
            return await resp.json()

# Run multiple coroutines concurrently
async def main():
    # Sequential (slow — waits for each)
    u1 = await fetch_user(1)
    u2 = await fetch_user(2)

    # Concurrent (fast — runs in parallel)
    u1, u2, u3 = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )

# Entry point for async code
asyncio.run(main())

# In FastAPI — framework handles the event loop for you
from fastapi import FastAPI
app = FastAPI()

@app.get("/users/{id}")
async def get_user(id: int):
    user = await fetch_user(id)
    return user
Ecosystem

Web Frameworks

FastAPI
Web / API
Modern async API framework. Auto-generates OpenAPI docs from type hints. Pydantic validation built-in. FastAPI if you know C#/Java — most familiar API style.
pip install fastapi uvicorn
Django
Full-Stack Web
Batteries-included full-stack framework. ORM, admin panel, auth, migrations — all built-in. Like ASP.NET Core MVC but with more conventions. Excellent for content-driven apps.
pip install django
Flask
Micro Web
Minimal microframework. Add only what you need. Great for simple APIs and prototyping. More manual than Django or FastAPI. Choose FastAPI over Flask for new projects.
pip install flask
Litestar
Web / API
Modern alternative to FastAPI with cleaner design, built-in dependency injection, and strong typing. Growing rapidly in 2025.
pip install litestar
SQLAlchemy
ORM / DB
The most powerful Python ORM. Core (SQL expression) + ORM layer. Works with PostgreSQL, MySQL, SQLite, MSSQL. Like EF Core but more explicit.
pip install sqlalchemy
Pydantic
Validation
Data validation using Python type hints. The backbone of FastAPI. Validates, serializes, and deserializes data. Like Fluent Validation but as data models.
pip install pydantic

FastAPI Quick Example (for the C# dev)

pythonmain.py — FastAPI resembles ASP.NET Core Minimal APIs
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from typing import Annotated

app = FastAPI(title="My API", version="1.0")

class Product(BaseModel):       # Like C# record / DTO
    name: str
    price: float
    category: str = "general"

@app.get("/products")           # like [HttpGet] in C#
async def get_products():
    return [{"id": 1, "name": "Laptop"}]

@app.get("/products/{id}")
async def get_product(id: int):  # Path params — typed automatically
    if id != 1: raise HTTPException(status_code=404)
    return {"id": 1, "name": "Laptop"}

@app.post("/products", status_code=201)
async def create_product(product: Product): # Auto-validates JSON body
    return {"id": 99, **product.dict()}

# Run: uvicorn main:app --reload
# OpenAPI docs auto at: http://localhost:8000/docs
Ecosystem

ML & AI Frameworks

PyTorch
Deep Learning
Facebook's deep learning framework. Dynamic computation graph — easier to debug. Dominant in research. Powers Hugging Face transformers and most LLM fine-tuning.
pip install torch
TensorFlow / Keras
Deep Learning
Google's deep learning framework. Keras provides the high-level API. Strong production deployment story (TF Serving, TFLite). More common in enterprise.
pip install tensorflow
scikit-learn
Classical ML
The foundation of classical ML. Linear models, decision trees, SVMs, clustering, preprocessing, cross-validation. Consistent fit/transform/predict API.
pip install scikit-learn
Hugging Face
LLMs / NLP
Transformers library — 100,000+ pretrained models. BERT, GPT, Llama fine-tuning, text classification, generation. The GitHub of AI models.
pip install transformers datasets
LangChain / LlamaIndex
LLM Apps
Frameworks for building LLM-powered applications. RAG pipelines, agents, tool use, memory. LlamaIndex focuses on data ingestion; LangChain on chaining.
pip install langchain llama-index
NumPy
Numerical
N-dimensional arrays and fast numerical operations. The foundation of all ML libraries. If you touch ML or data science, you touch NumPy.
pip install numpy
Ecosystem

Data Engineering & Science

pandas
Data Analysis
DataFrames for tabular data — like SQL tables in memory. Read CSV/Excel/JSON, filter, join, aggregate, reshape. Every data scientist uses it daily.
pip install pandas
Polars
Data Analysis
Modern pandas replacement built in Rust. 10–100× faster, lazy evaluation, better memory use. Learn this if starting fresh in 2025.
pip install polars
Apache Spark (PySpark)
Big Data
Distributed data processing for terabyte-scale. PySpark gives you Python API on Spark. Used in Databricks, AWS EMR, GCP Dataproc.
pip install pyspark
Apache Airflow
Workflow
Python-native workflow orchestration. Define DAGs (Directed Acyclic Graphs) for data pipelines. Dominant tool for ETL scheduling in data teams.
pip install apache-airflow
dbt
Transformation
Data transformation in SQL + Python. Transform data in your warehouse (Snowflake, BigQuery, Redshift). The modern T in ELT. Python models added in dbt-core 1.3+.
pip install dbt-core
Matplotlib / Seaborn / Plotly
Visualization
Matplotlib: low-level plotting. Seaborn: statistical charts on matplotlib. Plotly: interactive charts in Jupyter and web. Use plotly for anything modern.
pip install plotly
Jupyter
Notebooks
Interactive notebook environment. Code + markdown + output together. The standard for data exploration, ML experimentation, and reproducible research.
pip install jupyter
SQLAlchemy / Alembic
Database
SQLAlchemy: ORM + SQL expression language. Alembic: database migrations (like EF Core Migrations). Works with all major relational databases.
pip install sqlalchemy alembic
Ecosystem

Testing Frameworks

pythonpytest — the de-facto Python testing framework
# test_calculator.py
import pytest
from myapp.calculator import add, divide

# Simple test — no class needed!
def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, 1) == 0

# Parametrized tests (like [Theory, InlineData] in xUnit)
@pytest.mark.parametrize("a, b, expected", [
    (2, 3,  5),
    (0, 0,  0),
    (-1, 1, 0),
    (100, -50, 50),
])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected

# Test exceptions
def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

# Fixtures — like dependency injection for tests
@pytest.fixture
def sample_user():
    return {"id": 1, "name": "Alice", "email": "alice@test.com"}

@pytest.fixture
def db_session():
    # Setup
    session = create_test_db_session()
    yield session          # Give to test
    # Teardown (like [TearDown] in NUnit)
    session.close()

def test_create_user(db_session, sample_user):   # Fixtures injected by name
    result = create_user(db_session, sample_user)
    assert result["id"] is not None

# Run tests: pytest tests/ -v
# With coverage: pytest --cov=myapp --cov-report=html
Ecosystem

Dev Tooling

ToolCategoryEquivalent in C#/JavaInstall
blackFormatterdotnet format / prettierpip install black
ruffLinter + FormatterReSharper / StyleCoppip install ruff
mypyType checkerC# compiler checkspip install mypy
pytestTestingxUnit / NUnit / JUnitpip install pytest
PoetryPackage/dependency mgrNuGet / Mavenpip install poetry
uvUltra-fast pip replacementpip install uv
pyenvPython version managerSDK selectorcurl script
pre-commitGit hooksCustom build stepspip install pre-commit
loguruLoggingSerilog / NLogpip install loguru
httpxAsync HTTP clientHttpClientpip install httpx

pyproject.toml — The Modern Python Project File

tomlpyproject.toml (like .csproj + NuGet config)
[tool.poetry]
name = "my-app"
version = "1.0.0"
description = "My Python application"
python = "^3.12"

[tool.poetry.dependencies]
fastapi = "^0.110.0"
uvicorn = {extras = ["standard"], version = "^0.27"}"
sqlalchemy = "^2.0"
pydantic = "^2.6"

[tool.poetry.dev-dependencies]
pytest = "^8.0"
black = "^24.0"
ruff = "^0.3"
mypy = "^1.9"

[tool.black]
line-length = 88

[tool.ruff]
line-length = 88
select = ["E", "W", "F", "I"]   # PEP8 + isort

[tool.mypy]
python_version = "3.12"
strict = true
Reference

Quick Cheat Sheet

Syntax Mappings (C#/Java → Python)

C# / JavaPythonNotes
// comment# commentNo block comments, use # per line
/* block */"""docstring"""Triple quotes used as block comments
nullNoneAlways capitalize
true / falseTrue / FalseCapitalized!
&& / || / !and / or / notEnglish words, not symbols
i++ / i--i += 1 / i -= 1No ++ operator in Python
stringstrImmutable, 0-indexed, sliceable
List<T>listMixed types, dynamic size
Dictionary<K,V>dictKeys must be hashable
HashSet<T>setUnique, unordered
Console.WriteLine()print()f-strings for formatting
try/catch/finallytry/except/finallyexcept, not catch
throw new Exception()raise ValueError()raise, not throw
public/privatename / _name / __nameConvention only, not enforced
thisselfMust be explicit first param
new MyClass()MyClass()No new keyword!
x is Tisinstance(x, T)Type checking
foreach (x in xs)for x in xs:Iterates any iterable
String.Format()f"text {var}"f-strings (3.6+)

The Python Ecosystem at a Glance

FastAPI — Web API Django — Full-Stack Flask — Micro SQLAlchemy — ORM PyTorch — Deep Learning TensorFlow — DL (prod) scikit-learn — Classical ML Hugging Face — LLMs LangChain — LLM Apps pandas — DataFrames Polars — Fast DataFrames NumPy — Numerics PySpark — Big Data Airflow — Pipelines Jupyter — Notebooks pytest — Testing black — Formatting ruff — Linting mypy — Type Check Poetry — Dependencies pyenv — Versions uvicorn — ASGI server loguru — Logging httpx — HTTP client

PEP 8 Naming Conventions

ThingStyleExample
Variables, functionssnake_casemy_variable, calculate_total()
ClassesPascalCaseBankAccount, UserService
ConstantsUPPER_SNAKE_CASEMAX_RETRIES, API_KEY
Private (convention)_single_underscore_internal_value
Name mangled__double_underscore__id (becomes _ClassName__id)
Dunder / magic methods__name____init__, __str__, __eq__
Modules, packageslowercaseutils.py, data_access/