Python for the
Seasoned Developer
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.
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.
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.
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!
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.
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.
| Feature | C# / Java / PHP | Python |
|---|---|---|
| Typing | Static (declared types) | Dynamic (types at runtime) + optional type hints |
| Blocks | { } braces | Indentation (mandatory, 4 spaces) |
| Statement end | ; semicolons | Newline (no semicolons needed) |
| Entry point | Main() method | if __name__ == "__main__": |
| Null | null / NULL | None |
| Boolean | true / false | True / False (capitalised!) |
| Array | Fixed-type array / ArrayList | list — mixed types, dynamic size |
| HashMap / Dictionary | HashMap / Dictionary<K,V> | dict — built-in, first-class |
| String formatting | $"{var}" / String.format() | f"{var}" f-strings (same idea, cleaner) |
| Package manager | NuGet / Maven / Composer | pip / Poetry / uv |
| Access modifiers | public / private / protected | Convention: _private / __dunder |
| Interfaces | interface keyword | ABC (Abstract Base Classes) or duck typing |
| Generics | List<T> | Type hints: list[int] (3.9+) |
| Switch/case | switch statement | match (Python 3.10+) — much more powerful |
| Ternary | condition ? a : b | a if condition else b |
| Increment | i++ / i-- | No ++ / -- — use i += 1 |
| Compilation | Compiled to bytecode / binary | Interpreted (CPython) or compiled (.pyc bytecode) |
| GC | Managed GC | Reference counting + cyclic GC |
| Concurrency | Threads + async/await | GIL limits true threads; use asyncio or multiprocessing |
The 5 Things That Will Bite You Immediately
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.
++ operatorThere is no i++ in Python. Write i += 1. This surprises every Java/C# developer exactly once.
True and False are capitalizedtrue is a NameError. True is a boolean. Same for None (not null). Memorize it once, never again.
def func(items=[]): — this list is created ONCE and shared across all calls. Use None as default, then assign inside.
a = [1,2,3]; b = a — b is NOT a copy. Both a and b point to the same list. Use b = a.copy() or b = a[:].
"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.
Installation & Tools
# 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()
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.
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.
# 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
pip & Poetry
# 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
# 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.
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.
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); }
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.
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.
Variables & Dynamic Typing
# 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]
Built-in Data Types
| Type | Example | Mutable? | C# / Java Equivalent |
|---|---|---|---|
| int | 42, -10, 10_000_000 | No | int / long (unlimited size!) |
| float | 3.14, 1.5e10 | No | double |
| complex | 3+4j | No | No direct equivalent |
| bool | True, False | No | bool (subclass of int!) |
| str | "hello", 'world' | No | string |
| bytes | b"hello" | No | byte[] |
| list | [1, "a", True] | Yes | List<object> / ArrayList |
| tuple | (1, 2, 3) | No | ValueTuple / record |
| dict | {"key": "val"} | Yes | Dictionary<K,V> / HashMap |
| set | {1, 2, 3} | Yes | HashSet<T> |
| frozenset | frozenset({1,2}) | No | Immutable HashSet |
| NoneType | None | — | null |
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.
Strings & 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!
Numbers & Booleans
# 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")
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.
# 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]
Tuples — Immutable Records
# 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
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.
# 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}
Sets — Unique Unordered Collections
# 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)
if / elif / else
# 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'
Loops & Iterators
# 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!
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.
# 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
match — Structural Pattern Matching
# 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 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.
# 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
*args & **kwargs
# *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
Lambdas & 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
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.
# 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)
Classes & Objects
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
Inheritance & ABCs
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
Dataclasses — Goodbye Boilerplate
Dataclasses auto-generate __init__, __repr__, __eq__ and more from field declarations. Similar to C# records, Kotlin data classes, or Java records.
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
__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.
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()
# 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))
# 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())
Modules & Packages
# 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
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.
Error Handling
# 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
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.
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
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.
# 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)
Generators — Lazy Sequences
# 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]
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.
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
Web Frameworks
FastAPI Quick Example (for the C# dev)
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
ML & AI Frameworks
Data Engineering & Science
Testing Frameworks
# 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
Dev Tooling
| Tool | Category | Equivalent in C#/Java | Install |
|---|---|---|---|
| black | Formatter | dotnet format / prettier | pip install black |
| ruff | Linter + Formatter | ReSharper / StyleCop | pip install ruff |
| mypy | Type checker | C# compiler checks | pip install mypy |
| pytest | Testing | xUnit / NUnit / JUnit | pip install pytest |
| Poetry | Package/dependency mgr | NuGet / Maven | pip install poetry |
| uv | Ultra-fast pip replacement | — | pip install uv |
| pyenv | Python version manager | SDK selector | curl script |
| pre-commit | Git hooks | Custom build steps | pip install pre-commit |
| loguru | Logging | Serilog / NLog | pip install loguru |
| httpx | Async HTTP client | HttpClient | pip install httpx |
pyproject.toml — The Modern Python Project File
[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
Quick Cheat Sheet
Syntax Mappings (C#/Java → Python)
| C# / Java | Python | Notes |
|---|---|---|
// comment | # comment | No block comments, use # per line |
/* block */ | """docstring""" | Triple quotes used as block comments |
null | None | Always capitalize |
true / false | True / False | Capitalized! |
&& / || / ! | and / or / not | English words, not symbols |
i++ / i-- | i += 1 / i -= 1 | No ++ operator in Python |
string | str | Immutable, 0-indexed, sliceable |
List<T> | list | Mixed types, dynamic size |
Dictionary<K,V> | dict | Keys must be hashable |
HashSet<T> | set | Unique, unordered |
Console.WriteLine() | print() | f-strings for formatting |
try/catch/finally | try/except/finally | except, not catch |
throw new Exception() | raise ValueError() | raise, not throw |
public/private | name / _name / __name | Convention only, not enforced |
this | self | Must be explicit first param |
new MyClass() | MyClass() | No new keyword! |
x is T | isinstance(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
PEP 8 Naming Conventions
| Thing | Style | Example |
|---|---|---|
| Variables, functions | snake_case | my_variable, calculate_total() |
| Classes | PascalCase | BankAccount, UserService |
| Constants | UPPER_SNAKE_CASE | MAX_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, packages | lowercase | utils.py, data_access/ |