Python is a high-level, dynamically typed, interpreted language designed for readability, developer velocity, and cross-paradigm flexibility. Python prioritizes simplicity, readability, and developer experience over raw performance.
Python became popular because it let you write things fast: AI/ML (NumPy, PyTorch, TensorFlow), Web backends (FastAPI, Django, Flask), Automation & scripting, Data Science (Pandas, SciKit-Learn, Matplotlib), and for teaching beginner developers.
Key Language Features
Python is multi-paradigmatic supporting OOP, functional programming, and procedural programming.
- Dynamic Typing: No type declarations needed.
- Interpreted: No compilation step. Just run and go.
- Everything is an object: Literally, including functions and modules.
- First-class functions: supports closures, lambdas, and decorators.
- Indentation-based blocks: No
{}—just colons and indentation.
Typing and Modern Python
Since Python 3.5+, optional type hints have enabled gradual typing.
def greet(name: str) -> str:
return f"Hello, {name}"- Use
mypy,pydantic, orPyrightto enforce type contracts.
The “dunder” API
Behind every intuitive behavior is a magic method (double underscore methods like __init__, __str__, __getitem__, __call__, etc.)
This API makes Python incredibly flexible for metaprogramming and DSLs.
Example: making a class iterable:
class MyRange:
def __init__(self, end): self.i, self.end = 0, end
def __iter__(self): return self
def __next__(self):
if self.i >= self.end: raise StopIteration
val = self.i
self.i += 1
return valTooling and Ecosystem
venv: Isolated environments.pip: Package management.black, isort, ruff: Linting and formatting.pytest, hypothesis: Testingjupyter: Interactive data workflows.poetry, pipenv: Modern dependency + env management.pydantic: Declarative data validation.asyncio, httpx, uvicorn: Async web stack.
Potential Gotchas for Experienced Devs
- Indentation quirks: Mixing tabs and spaces can be painful.
- Truthiness:
[], 0, None, '', {}are all false. - Lack of visibility: No private/public keywords. Use
_/__conventions. - Python vs 3 was a decade-long mess. Today use Python 3.12+.
Important Data Types
Strings
f-strings: string interpolation, inline expressions, & number formatting
name = "Alex"
f"Hello, {name}!" # → "Hello, Alex!"
f"2 + 2 = {2 + 2}" # → "2 + 2 = 4"
pi = 3.14159
f"{pi:.2f}" # → "3.14"Lists
List functions: length, slicing, sorting
my_list = [4, 3, 2, 1]
len(my_list) # 4
my_list[1:2] # [3, 2]
my_list.sort() # [1, 2, 3, 4]Manipulating lists: append, insert, remove, pop
my_list = [1, 2, 3]
my_list.append(9) # [1, 2, 3, 9]
my_list.insert(2, 7) # [1, 2, 7, 3, 9]
my_list.remove(7) # [1, 2, 3, 9]
my_list.pop(0) # [2, 3, 9]Sets
Sets are useful for a de-duplicated list.
my_set = {1, 2, 2, 2, 3, 4}
print(my_set) # {1, 2, 3 ,4}
for x in my_set:
print(x) # sets don't guarantee order, will likely print out-of-order
my_set.discard(4) # {1, 2, 3}
my_set.add(9) # {1, 2, 3, 9}
my_set.update([7, 8]) # {1, 2, 3, 9, 7, 8}Tuples
Tuples are immutable lists.
my_tuple = (1, 2, 3)
print(my_tuple) #(1, 2, 3)
print(my_tuple[1]) # 2Dictionaries
user_dictionary = {
'name': 'Alexa'
'age': 32
}
print(user_dictionary) # {'name': 'Alexa', 'age': 32}
user_dictionary.get("name") # Alexa
user_dictionary["location"] = 'NYC'
user_dictionary.pop("age") # removes age
user_dictionary.clear() # {}
del user_dictionary # completely deletes the dictionaryFunctions
def print_name(name):
print(f"Hello {name}")
print_name("Alexa") # Hello AlexaPackages and Imports
- Import is the act of bringing code into your file from another module or package.
- A module is any .py file.
- A package is a folder that contains an
__init.py__file. It can contain multiple modules or sub-packages.
my_project/
└── utils/ (package)
├── __init__.py
└── helpers.py (module)
Import styles ranked by most recommended
In Python, you use import to bring in code from another module or package—whether it’s built-in, third-party, or your own.
import module
Importing an entire module adds only the module name (e.g. math) to your namespace. You must access its functions or constants using the module prefix. This is recommended for its clarity and explicit origin of very symbol, reducing naming collisions and encourages modular thinking.
import math
math.sqrt(16)
math.pifrom module import symbol
Imports specific symbols from a module. This makes a module prefix unnecessary and improves concision. However, this can lead to name collisions if overused. This is most ideal for common utility functions (from os.path import join) or frequently used classes (from datetime import datetime).
from math import sqrt
sqrt(16)from module as alias
Aliases import the the whole module. Use aliases only when the alias is well-known! (e.g. np, pd)
import numpy as np
np.array([1,2,3])from .module import name and from module import *
Avoid these, especially the wildcard import. These lack safety and readability. Relative imports can be useful for internal packages where avoiding circular imports is necessary. Wildcard imports import everything from a module into your local namespace.
# Relative imports
from .helpers import foo
from ..core.utils import barfrom math import * # Wildcard imports are bad practice!Objects
Everything is an object in Python— even literals like 42, “hello” and functions and modules.
class Dog:
def __init__(self, name):
# instance variables
self.name = name
self.__age = age # double underscore means "private"
def bark(self):
return f"{self.name} says woof!"
@property # getter for age
def age(self):
return self.__age
@age.setter # property: setter for age
def age(self, value):
if value < 0:
raise ValueError("Age must be non-negative")
self.__age = value
# dunder method for string representation
def __str__(self):
return f"{self.name} is {self.age()} years old"fido = Dog("Fido", 3)
print(fido.bark()) # "Fido says woof!"
print(fido.age) # 3 (accesses via @property)
fido.age = 4 # setter
print(fido) # "Fido is 4 years old"Inheritance & Method Override
class Animal:
def __init__(self, name):
self.name = name
def utter(self):
return f"{self.name} makes a sound"class Cat(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def utter(self):
return f"{self.name} says meow"Access Control in Python (Encapsulation)
While languages like Swift have explicit access control modifiers (public, internal private), Python uses naming conventions only. This means programmers can call anything and are trusted to follow convention.
| Prefix | Meaning |
|---|---|
foo | Public—accessible anywhere. e.g. `x.publicVa |
_foo | Internal—accessible but don’t call externally. e.g. `x._internalV |
__foo Private—name-mangled. Cannot call x.__privateVar but still accessible as self__.privateVar hin |
Special “Dunder” Methods
These special methods let you customize object behavior and hook into Python’s syntax.
class Vector:
def __init__(self, x): self.x = x
def __add__(self, other): return Vector(self.x + other.x)
def __repr__(self): return f"Vector({self.x})"v1 = Vector(1)
v2 = Vector(2)
v1 + v2 # Vector(3)| Method | Purpose |
|---|---|
__init__ | Constructor |
__repr__ | Debug string |
__len__ | len(obj) |
__getitem__ | obj[key] |
__setitem__ | obj[key] = val |
__eq__ | Equality check (==) |
| etc… |
@dataclass
If you just need a container for related data (like a Swift struct), Python gives you two idiomatic options:
- Classic class
class User:
def __init__(self, name, age):
self.name = name
self.age = age- @dataclass (Python 3.7+)
- Auto-generates
__init__,__repr__,__eq__, etc. - Great for Plain Old Python Objects without all the boilerplate and slightly better behavior
from dataclasses import dataclass
@dataclass
class User:
name: str
age: intDuck (Structural) Typing & Interfaces
In Python, what matters is what an object can do, not what it says it can do.
- There’s no
interfacekeyword, no need to conform to a named protocol. - If an object has the right method(s), you can use it.
class User:
def to_json(self): return '{"type": "user"}'
class Product:
def to_json(self): return '{"type": "product"}'
def serialize(obj):
return obj.to_json() # Both User & Product work, no interface requiredIn Python 3.8+, you can write interface-like constructs if you want IDE/linter support. This doesn’t affect runtime behavior and you don’t explicitly need to implement it, just have the methods.
from typing import Protocol
class JsonSerializable(Protocol):
def to_json(self) -> str: ...
def serialize(obj: JsonSerializable): ...