Indentation enforces structure.
No braces, no semicolons.
my_module.py
mypackage
Lowercase letters with underscore; avoid starting with number. (Note: Java uses camelCase)
user_name = "Alice"
total_score = 95
def calculate_total(price_list):
return sum(price_list)
First letter of each word capitalized, no underscores.
class CustomerProfile:
pass
Typically defined at module level.
PI = 3.14159
MAX_USERS = 100
Single underscore _var → "internal use" (protected)
Intended for inside the class or module; not part of the stable public interface.
Double underscore __var → name mangling (private-like)
Name mangling = Automatic renaming of __var → _ClassName__var to avoid accidental access and subclass name collisions.
class Example:
def __init__(self):
self._internal_var = 42 # convention: protected
self.__private_var = 99 # name mangled: class-specific
Names that both start and end with double underscores: __name__
They are part of Python's data model and have special meaning that Python itself uses.
❌ You should not create your own arbitrary __myfunc__ (Unless you're intentionally integrating with Python's object model)
✅ We override existing dunder methods to customize behavior.
class Greeter:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Hello, {self.name}!"
def __call__(self):
print(f"Greetings, {self.name}!")
A docstring (documentation string) is a string literal that appears as the first statement in a module, function, class, or method definition.
They make your code self-documenting
Tools can auto-generate documentation from them
IDEs show them as tooltips when you hover over functions
The docstring is enclosed in triple quotes (""" or ''') and can span multiple lines.
def square(x):
"""Return the square of x."""
return x * x
def calculate_average(numbers):
"""
Calculate the average of a list of numbers.
Args:
numbers (list): A list of numeric values.
Returns:
float: The average of the numbers.
Raises:
ValueError: If the list is empty.
"""
if not numbers:
raise ValueError("Cannot calculate average of empty list")
return sum(numbers) / len(numbers)
Google Style - uses sections like Args, Returns, Raises
NumPy Style - similar but with different formatting
Sphinx/reStructuredText - uses :param:, :return: syntax
You can access docstrings using the __doc__ attribute or the help() function:
print(greet.__doc__) # Prints: Return a greeting message for the given name.
help(greet) # Shows formatted documentation
A module is a single Python file (.py) that contains code (functions, classes, variables)
Creating a module: math_utils.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
PI = 3.14159
Using a module with import:
import math_utils
result = math_utils.add(5, 3)
print(result) # 8
print(math_utils.PI) # 3.14159
Alternative import styles:
from math_utils import add, PI # Import specific items, that can be used directly
from math_utils import * # Import everything (not recommended)
import math_utils as mu # Use an alias
A package is a directory containing multiple modules, organized hierarchically.
It must contain a special __init__.py file (can be empty) that tells Python to treat the directory as a package.
Package structure example:
mypackage/
__init__.py
module1.py
module2.py
subpackage/
__init__.py
module3.py
Using a package:
import mypackage.module1
from mypackage.subpackage import module3
The __init__.py file can be empty or contain initialization code that runs when the package is imported. It can also define what gets imported with from package import *
Example: mypackage/__init__.py:
from .module1 import some_function
from .module2 import SomeClass
__all__ = ['some_function', 'SomeClass']
All values are objects. Even primitive-looking things like int, float, bool, functions, and modules are objects of classes.
x = 5
print(type(x)) # <class 'int'>
Classes themselves are objects (instances of type):
print(type(int)) # <class 'type'>
Meaning: Python’s runtime is deeply dynamic. Creating classes, modifying them, attaching attributes, etc., can happen at execution time.
Encapsulation is not enforced, just convention. Philosophy: "We're all adults here"
No true private/protected access modifiers.
No final. No const.
_var: "Internal use" / protected by convention
__var: Name-mangled to avoid accidental external access
Dynamic typing: variable names don’t have fixed types.
x = 5
x = "hello" # allowed (x now references a string)
Strong typing: Python will not implicitly convert types.
5 + "5" # TypeError (strong typing)
Mutable objects (lists, dicts, sets, custom objects): Modifying the object affects all references.
x = [1, 2, 3]
y = x
x.append(4) # Modifies the list object
print(y) # [1, 2, 3, 4] - same object
Immutable objects (integers, floats, strings, tuples, booleans): You can't modify the object itself. Operations that seem to "change" them actually create new objects.
a = "hello"
b = a
a = a + " world" # Creates new string "hello world"
print(b) # "hello" - still points to original
"If it walks like a duck and quacks like a duck, it's a duck."
No need to implement interfaces explicitly.
def make_sound(animal):
animal.sound() # Just call it; no type checking
As long as the object provides .sound(), it works. (runtime Polymorphism)
Python only checks at runtime whether the object has a .sound() method.
Note: In Python, the expected behavior is usually communicated through docstrings, not the type signature:
def make_sound(animal):
"""
Calls animal.sound().
Parameters:
animal: Any object that has a .sound() method.
"""
animal.sound()
Python now supports type hints that document expected behavior, but they still enforce duck typing—not inheritance.
from typing import Protocol
class SoundMaker(Protocol):
def sound(self) -> None:
pass
def make_sound(animal: SoundMaker) -> None:
animal.sound()
SoundMaker defines an interface — a contract that says "any object with a sound() method (that takes no arguments and returns None) satisfies this protocol."
class Dog:
def sound(self) -> None:
print("Woof!")
class Cat:
def meow(self) -> None: # Different method name
print("Meow!")
make_sound(Dog()) # ✓ Works - has sound() method
make_sound(Cat()) # ✗ Type checker error - no sound() method
It's compile-time duck typing with type safety.
# Hint: takes two int args and returns an int
def add(x: int, y: int) -> int:
return x + y
# Hint: takes one str arg and returns none
def print_message(msg: str) -> None:
print(msg)
# Hint: takes no args and returns a list of int-s
def get_data() -> list[int]:
return [1, 2, 3]
Type annotations (like -> int or x: int) are completely optional. They're just hints for:
Type checkers (like mypy, pyright) — catch type errors before runtime
IDEs — better autocomplete and error detection
Documentation — help other developers understand your code
In Python, functions are first-class objects. This means you can:
Store them in variables
def greet():
print("Hi!")
say_hello = greet # assign function to another variable
say_hello() # still works
Pass them as arguments
def apply_twice(func):
func()
func()
apply_twice(greet)
Return them from functions
import math
def get_math_func(name):
if name == "sqrt":
return math.sqrt
elif name == "floor":
return math.floor
else:
return abs
func = get_math_func("sqrt")
print(func(16)) # 4.0
Add attributes to them
def count_calls():
x = 0 # local attribute
x += 1
count_calls.times += 1 # function attribute
count_calls.times = 0 # Add an attribute to the function
count_calls() # count_calls.time = 1
count_calls() # count_calls.time = 2
count_calls.new_attr = "Hello" # Add a new attribute to the function
Functions are objects, so they can have attributes just like class instances.
Python supports:
dynamic attribute addition
runtime code modification
introspection/reflection
obj.new_attr = 10
Metaprogramming is natural in Python (decorators, metaclasses).
A class creates objects. A metaclass creates classes.
In Python, type is the built-in metaclass:
class MyClass:
pass
print(type(MyClass)) # <class 'type'>
Meaning: Classes themselves are objects created by type.
Metaclasses let you modify how classes are built.
Use cases:
Enforcing naming conventions
Auto-registering classes
Adding methods automatically
Implementing ORM class declarations (like Django models)