These ten concepts appear in almost every Python project. If you know all of them, you can read and write real Python code. If you are missing one, that is the one that trips you up on every project.
1. Variables and Data Types
Python infers types automatically. No declaration needed.
name: str = "Alex"
age: int = 25
score: float = 9.5
active: bool = True
nothing = None
print(type(name)) # <class 'str'>
The four built-in collection types:
numbers = [1, 2, 3] # list — ordered, mutable
point = (10, 20) # tuple — ordered, immutable
tags = {"python", "dev"} # set — unique items
user = {"name": "Alex", "age": 25} # dict — key/value pairs
Use type() to check the type of any variable at runtime. Use type hints for documentation and IDE support.
2. Functions
Define functions with def. Use return to send a value back. If you skip return, the function returns None.
def greet(name: str = "World") -> str:
return f"Hello, {name}!"
print(greet()) # Hello, World!
print(greet("Alex")) # Hello, Alex!
*args and **kwargs make functions flexible:
def total(*args: int) -> int:
return sum(args)
def profile(**kwargs: str) -> dict:
return dict(kwargs)
total(1, 2, 3) # 6
profile(name="Alex", city="Berlin") # {'name': 'Alex', 'city': 'Berlin'}
Functions are first-class objects. You can store them in variables, pass them to other functions, or return them.
3. Classes and OOP
A class is a blueprint for creating objects. __init__ runs when you create one.
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self) -> str:
return f"{self.name} barks"
dog = Dog("Rex")
print(dog.speak()) # Rex barks
Use @property for computed attributes:
@property
def name_upper(self) -> str:
return self.name.upper()
Use super() to call the parent class method from an override.
4. List Comprehensions
Create lists, dicts, and sets in one line.
numbers = [1, 2, 3, 4, 5, 6]
evens = [x for x in numbers if x % 2 == 0] # [2, 4, 6]
doubled = [x * 2 for x in numbers] # [2, 4, 6, 8, 10, 12]
# Dict comprehension
squares = {x: x**2 for x in range(6)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Generator expression — lazy, saves memory
gen = (x * 2 for x in range(1_000_000))
print(next(gen)) # 0
Comprehensions are faster than for loops and more readable for simple transformations. Keep nested comprehensions shallow — two levels is usually the limit before readability suffers.
5. Decorators
A decorator wraps a function to add behavior without changing the function’s code.
import functools, time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.3f}s")
return result
return wrapper
@timer
def slow_task():
time.sleep(0.1)
slow_task() # slow_task took 0.100s
Always use @functools.wraps(func) inside decorators. Without it, the wrapped function loses its __name__ and __doc__.
You can stack decorators. They apply from bottom to top:
@timer
@functools.lru_cache(maxsize=128)
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
6. Generators
A generator produces values one at a time using yield. It pauses after each yield and resumes when you ask for the next value.
def count_up(limit: int):
n = 0
while n < limit:
yield n
n += 1
for num in count_up(3):
print(num) # 0, 1, 2
Generators are memory-efficient. A list of one million numbers takes ~8 MB. A generator of the same numbers takes almost nothing because only one value exists in memory at a time.
def integers():
n = 0
while True:
yield n
n += 1
# Chain generators into a pipeline
def evens(src):
return (x for x in src if x % 2 == 0)
7. Error Handling
Use try / except / else / finally:
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}")
else:
print(f"Result: {result}") # runs only if no exception
finally:
print("Always runs") # cleanup goes here
Create custom exceptions by inheriting from Exception:
class InsufficientFunds(Exception):
def __init__(self, amount: float) -> None:
super().__init__(f"Need {amount:.2f} more")
def withdraw(balance: float, amount: float) -> float:
if amount > balance:
raise InsufficientFunds(amount - balance)
return balance - amount
Never swallow exceptions silently. Always log or re-raise them.
8. Type Hints
Type hints do not enforce types at runtime. Tools like mypy and your IDE use them to catch bugs before you run the code.
from typing import Optional, Union, TypedDict
from collections.abc import Callable
def find_user(id: int) -> Optional[str]:
return None
# Python 3.10+: use | instead of Union
def parse(value: str | int) -> str:
return str(value)
class User(TypedDict):
name: str
age: int
Handler = Callable[[str, int], bool]
Run mypy script.py to check types. Add it to your CI pipeline to catch type errors automatically.
9. Modules and Packages
A module is a Python file. A package is a directory with an __init__.py.
# math_utils.py
__all__ = ["add", "subtract"] # public API
def add(a: int, b: int) -> int:
return a + b
def subtract(a: int, b: int) -> int:
return a - b
# main.py
import math_utils
from math_utils import add
print(add(3, 4)) # 7
Package layout:
myapp/
├── __init__.py
└── utils/
├── __init__.py
└── strings.py
from myapp.utils.strings import format_name
Use __all__ to define the public API. Avoid circular imports — they cause hard-to-debug errors.
10. Async / Await
Async lets your code do other work while waiting for I/O (network, files, databases).
import asyncio
async def fetch(url: str) -> str:
await asyncio.sleep(1) # simulate network call
return f"data from {url}"
async def main() -> None:
# Run both concurrently — not sequentially
results = await asyncio.gather(
fetch("api.example.com/users"),
fetch("api.example.com/posts"),
)
for r in results:
print(r)
asyncio.run(main())
# Finishes in ~1s, not ~2s
asyncio.gather() runs coroutines concurrently. One task pauses at await, and another resumes. This is cooperative, not parallel — only one task runs at a time.
For CPU-heavy work, use threading or multiprocessing instead.