Premium
Log in
Python provides several built-in data types:
Python 3.13# Explicit type conversion x = int("42") # str → int y = float(42) # int → float z = str(3.14) # float → str w = bool(0) # int → bool (False) # Type checking print(type(x)) # <class 'int'>
Python 3.13fruits = ["apple", "banana", "cherry"] fruits.append("pear") # Add element fruits[0] = "orange" # Modify element
Python 3.13point = (10, 20) # point[0] = 5 # TypeError — cannot modify # Tuple as a dictionary key locations = {(55.75, 37.62): "Moscow"}
Python 3.13colors = {"red", "green", "blue"} colors.add("yellow") colors.discard("red")
Python 3.13immutable_set = frozenset([1, 2, 3]) # immutable_set.add(4) # AttributeError # frozenset as a dictionary key cache = {frozenset([1, 2]): "result"}
Python 3.13a = {1, 2, 3, 4} b = {3, 4, 5, 6} a | b # Union: {1, 2, 3, 4, 5, 6} a & b # Intersection: {3, 4} a - b # Difference: {1, 2} a ^ b # Symmetric difference: {1, 2, 5, 6}
set is used for fast deduplication and membership checks (in runs in O(1)). frozenset is needed when a set must serve as a dictionary key or an element of another set.
Python 3.13user = { "name": "Anna", "age": 25, "city": "Moscow" }
Python 3.13user["name"] # Access by key (KeyError if missing) user.get("email", "—") # Safe access with default value user.keys() # All keys user.values() # All values user.items() # Key-value pairs user.pop("city") # Remove and return value user.update({"age": 26}) # Update values
Python 3.13# Literal d1 = {"a": 1, "b": 2} # From a list of tuples d2 = dict([("a", 1), ("b", 2)]) # Using dict comprehension d3 = {x: x ** 2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
When "modified," a new object is created:
Python 3.13x = 10 print(id(x)) # e.g.: 140234866357520 x += 1 print(id(x)) # Different id — this is a new object
The object is modified "in place":
Python 3.13lst = [1, 2, 3] print(id(lst)) # e.g.: 140234866400064 lst.append(4) print(id(lst)) # Same id — object was modified
Python 3.13# Common mistake def add_item(item, lst=[]): # Same list across all calls! lst.append(item) return lst # Correct approach def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst
Python 3.13text = "Python" text[0] # 'P' text[-1] # 'n' text[-2] # 'o'
Syntax: [start:stop:step]
Python 3.13nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] nums[2:5] # [2, 3, 4] nums[:3] # [0, 1, 2] nums[7:] # [7, 8, 9] nums[::2] # [0, 2, 4, 6, 8] — every second nums[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] — reverse
Python 3.13text = "Hello, World!" text[7:12] # 'World' text[::-1] # '!dlroW ,olleH' coords = (10, 20, 30, 40, 50) coords[1:4] # (20, 30, 40)
Python offers several ways to format strings:
Available since Python 3.6. Allow embedding expressions directly in the string.
Python 3.13name = "Anna" age = 25 print(f"Hello, {name}! You are {age} years old.") print(f"In 5 years you will be {age + 5}.") print(f"Name in uppercase: {name.upper()}")
Python 3.13print("Hello, {}! You are {} years old.".format(name, age)) print("Hello, {0}! {0}, you are {1} years old.".format(name, age))
Python 3.13print("Hello, %s! You are %d years old." % (name, age))
Python 3.13pi = 3.14159265 print(f"Pi: {pi:.2f}") # Pi: 3.14 print(f"Number: {1000000:,}") # Number: 1,000,000 print(f"Percent: {0.856:.1%}") # Percent: 85.6%
Conditional statements allow you to execute different blocks of code depending on a condition.
Python 3.13age = 18 if age < 13: print("Child") elif age < 18: print("Teenager") else: print("Adult")
A concise way to write a condition in a single line:
Python 3.13status = "adult" if age >= 18 else "minor"
In Python, the following values are considered false (Falsy):
Everything else is considered true (Truthy):
Python 3.13items = [] if items: print("List is not empty") else: print("List is empty") # This block will execute
Python supports chained comparisons:
Python 3.13x = 5 if 1 < x < 10: print("x is in the range from 1 to 10")
Iterates over the elements of an iterable object (list, string, range, etc.):
Python 3.13fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit) # Loop over a range of numbers for i in range(5): print(i) # 0, 1, 2, 3, 4
Executes as long as the condition is true:
Python 3.13count = 0 while count < 3: print(count) count += 1
Python 3.13for i in range(10): if i == 3: continue # Skips 3 if i == 7: break # Stops the loop at 7 print(i) # 0, 1, 2, 4, 5, 6
Executes if the loop completes without encountering a break:
Python 3.13for n in range(2, 10): for x in range(2, n): if n % x == 0: break else: # Executes if break was not triggered print(f"{n} is a prime number")
Python 3.13def greet(name): return f"Hello, {name}!" message = greet("Anna") print(message) # Hello, Anna!
Python 3.13# Default values def power(base, exponent=2): return base ** exponent power(3) # 9 (exponent = 2) power(3, 3) # 27 (exponent = 3)
Python 3.13def create_user(name, age, city="Moscow"): return {"name": name, "age": age, "city": city} # Keyword arguments can be passed in any order user = create_user(age=25, name="Anna")
Python 3.13def min_max(numbers): return min(numbers), max(numbers) lo, hi = min_max([3, 1, 7, 2, 9]) print(lo, hi) # 1 9
If return is absent, the function returns None:
Python 3.13def say_hello(name): print(f"Hello, {name}!") result = say_hello("World") print(result) # None
Collects all extra positional arguments into a tuple:
Python 3.13def total(*args): return sum(args) total(1, 2, 3) # 6 total(10, 20) # 30
Collects all extra keyword arguments into a dictionary:
Python 3.13def build_profile(**kwargs): return kwargs build_profile(name="Anna", age=25, city="Moscow") # {'name': 'Anna', 'age': 25, 'city': 'Moscow'}
The order of parameters in a function definition is strictly fixed: regular → *args → keyword-only → **kwargs:
Python 3.13def func(a, b, *args, **kwargs): print(f"a={a}, b={b}") print(f"args={args}") print(f"kwargs={kwargs}") func(1, 2, 3, 4, x=10, y=20) # a=1, b=2 # args=(3, 4) # kwargs={'x': 10, 'y': 20}
Python 3.13def greet(name, age): print(f"{name}, {age} years old") args_list = ["Anna", 25] greet(*args_list) # Unpacking a list kwargs_dict = {"name": "Ivan", "age": 30} greet(**kwargs_dict) # Unpacking a dictionary
Python 3.13# Regular function def square(x): return x ** 2 # Equivalent lambda square = lambda x: x ** 2 square(5) # 25
Python 3.13users = [ {"name": "Anna", "age": 25}, {"name": "Boris", "age": 30}, {"name": "Vera", "age": 20}, ] # Sort by age sorted_users = sorted(users, key=lambda u: u["age"])
Python 3.13numbers = [1, 2, 3, 4, 5] squares = list(map(lambda x: x ** 2, numbers)) # [1, 4, 9, 16, 25] evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]
Python 3.13x = "global" # Global def outer(): x = "enclosing" # Enclosing def inner(): x = "local" # Local print(x) # "local" inner() outer()
Allows modifying a global variable inside a function:
Python 3.13counter = 0 def increment(): global counter counter += 1 increment() print(counter) # 1
Allows modifying a variable from an outer (enclosing) function:
Python 3.13def outer(): count = 0 def inner(): nonlocal count count += 1 return count return inner counter = outer() print(counter()) # 1 print(counter()) # 2
Python 3.13[expression for item in iterable]
Python 3.13# Squares of numbers squares = [x ** 2 for x in range(6)] # [0, 1, 4, 9, 16, 25] # Equivalent with a loop squares = [] for x in range(6): squares.append(x ** 2)
Python 3.13# Only even numbers evens = [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]
Python 3.13labels = ["even" if x % 2 == 0 else "odd" for x in range(5)] # ['even', 'odd', 'even', 'odd', 'even']
Python 3.13# Multiplication table matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)] # [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
Python 3.13# Dict comprehension squares_dict = {x: x ** 2 for x in range(5)} # Set comprehension unique_lengths = {len(word) for word in ["cat", "dog", "fox"]}
Avoid overusing complex nested comprehensions — if the expression is hard to read, use a regular loop instead.
Python 3.13def count_up_to(n): i = 1 while i <= n: yield i i += 1 for num in count_up_to(5): print(num) # 1, 2, 3, 4, 5
Similar to a list comprehension, but uses parentheses:
Python 3.13# List comprehension — creates the whole list in memory squares_list = [x ** 2 for x in range(1000000)] # Generator expression — evaluates one by one squares_gen = (x ** 2 for x in range(1000000))
Python 3.13gen = (x for x in range(3)) print(list(gen)) # [0, 1, 2] print(list(gen)) # [] — already exhausted
Python 3.13numbers = [1, 2, 3, 4, 5] squares = list(map(lambda x: x ** 2, numbers)) # [1, 4, 9, 16, 25] # Equivalent using comprehension squares = [x ** 2 for x in numbers]
Python 3.13numbers = [1, 2, 3, 4, 5, 6] evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4, 6] # Equivalent using comprehension evens = [x for x in numbers if x % 2 == 0]
Python 3.13names = ["Anna", "Boris", "Vera"] ages = [25, 30, 22] pairs = list(zip(names, ages)) # [('Anna', 25), ('Boris', 30), ('Vera', 22)] # Often used to create a dictionary user_ages = dict(zip(names, ages)) # {'Anna': 25, 'Boris': 30, 'Vera': 22}
Python 3.13a, b, c = [1, 2, 3] print(a, b, c) # 1 2 3 # Works with tuples, strings, and other iterables x, y = (10, 20) first, second, third = "abc"
Python 3.13a, b = 1, 2 a, b = b, a print(a, b) # 2 1
Collects the "remaining" items into a list:
Python 3.13first, *rest = [1, 2, 3, 4, 5] print(first) # 1 print(rest) # [2, 3, 4, 5] first, *middle, last = [1, 2, 3, 4, 5] print(middle) # [2, 3, 4]
Python 3.13def greet(name, age, city): print(f"{name}, {age}, {city}") data = ["Anna", 25, "Moscow"] greet(*data) # Unpacking a list info = {"name": "Ivan", "age": 30, "city": "St. Petersburg"} greet(**info) # Unpacking a dictionary
Python 3.13points = [(1, 2), (3, 4), (5, 6)] for x, y in points: print(f"x={x}, y={y}")
Python 3.13try: result = 10 / 0 except ZeroDivisionError: print("Division by zero!")
Python 3.13try: number = int(input("Enter a number: ")) except ValueError: print("That's not a number!") else: # Executes if NO exception occurred print(f"You entered: {number}") finally: # ALWAYS executes print("Shutting down")
Python 3.13try: value = int("abc") except (ValueError, TypeError) as e: print(f"Error: {e}")
Catch specific exceptions instead of a generic except Exception — this helps avoid hiding unexpected errors.
Python 3.13class InsufficientFundsError(Exception): pass def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError("Insufficient funds") return balance - amount try: withdraw(100, 200) except InsufficientFundsError as e: print(e) # Insufficient funds
Python 3.13class ValidationError(Exception): def __init__(self, field, message): self.field = field self.message = message super().__init__(f"{field}: {message}") try: raise ValidationError("email", "Invalid format") except ValidationError as e: print(e.field) # email print(e.message) # Invalid format
Python 3.13class AppError(Exception): """Base application exception""" pass class NotFoundError(AppError): pass class AccessDeniedError(AppError): pass
The most common example is working with files:
Python 3.13# Without with — you must remember to close the file file = open("data.txt", "r") try: content = file.read() finally: file.close() # With with — the file gets closed automatically with open("data.txt", "r") as file: content = file.read() # The file is already closed
Python 3.13class Timer: def __enter__(self): import time self.start = time.time() return self def __exit__(self, exc_type, exc_val, exc_tb): import time elapsed = time.time() - self.start print(f"Execution time: {elapsed:.2f} sec") return False # Don't suppress exceptions with Timer(): total = sum(range(1_000_000)) # Execution time: 0.03 sec
The open() function takes the path to the file and a mode:
Python 3.13# Read the entire file with open("data.txt", "r", encoding="utf-8") as f: content = f.read() # Read line by line with open("data.txt", "r", encoding="utf-8") as f: for line in f: print(line.strip()) # Read all lines into a list with open("data.txt", "r", encoding="utf-8") as f: lines = f.readlines()
Python 3.13# Overwrite the file with open("output.txt", "w", encoding="utf-8") as f: f.write("First line\n") f.write("Second line\n") # Append to the end with open("output.txt", "a", encoding="utf-8") as f: f.write("Another line\n")
Always specify encoding="utf-8" to avoid issues with special characters and non-Latin alphabets:
Python 3.13# Without specifying encoding, a UnicodeDecodeError might occur with open("data.txt", "r", encoding="utf-8") as f: content = f.read()
Always use the with statement — it ensures the file is properly closed even if an error occurs.
Python 3.13class Dog: # Class attribute (shared by all instances) species = "Canis familiaris" # Constructor — gets called when creating an object def __init__(self, name, age): # Instance attributes (unique to each object) self.name = name self.age = age # Instance method def bark(self): return f"{self.name} says: Woof!"
Python 3.13dog1 = Dog("Bobik", 3) dog2 = Dog("Sharik", 5) print(dog1.name) # Bobik print(dog2.bark()) # Sharik says: Woof! print(dog1.species) # Canis familiaris
Python 3.13class Counter: count = 0 # Class attribute — shared by all def __init__(self): Counter.count += 1 # Modify the class attribute self.id = Counter.count # Instance attribute c1 = Counter() c2 = Counter() print(Counter.count) # 2 print(c1.id, c2.id) # 1 2
Python 3.13class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name} makes a sound" class Dog(Animal): def speak(self): return f"{self.name} says: Woof!" class Cat(Animal): def speak(self): return f"{self.name} says: Meow!" dog = Dog("Bobik") print(dog.speak()) # Bobik says: Woof!
Python 3.13class Animal: def __init__(self, name, age): self.name = name self.age = age class Dog(Animal): def __init__(self, name, age, breed): super().__init__(name, age) # Call parent's __init__ self.breed = breed dog = Dog("Bobik", 3, "Labrador") print(dog.name, dog.breed) # Bobik Labrador
Python 3.13print(isinstance(dog, Dog)) # True print(isinstance(dog, Animal)) # True print(issubclass(Dog, Animal)) # True
A child class can replace or extend a parent's method:
Python 3.13class Shape: def area(self): return 0 class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height rect = Rectangle(5, 3) print(rect.area()) # 15
Python supports inheriting from multiple classes. The method lookup order is determined by the MRO (Method Resolution Order) algorithm, which can be inspected via ClassName.mro().
In Python, there are no strict access modifiers (private, public). Instead, conventions are used:
Python 3.13class BankAccount: def __init__(self, balance): self.__balance = balance # "Private" attribute def get_balance(self): return self.__balance account = BankAccount(1000) # print(account.__balance) # AttributeError print(account.get_balance()) # 1000 # Name mangling — the attribute is accessible via the modified name print(account._BankAccount__balance) # 1000
Allows using methods as if they were attributes:
Python 3.13class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Below absolute zero!") self._celsius = value @property def fahrenheit(self): return self._celsius * 9 / 5 + 32 temp = Temperature(25) print(temp.celsius) # 25 print(temp.fahrenheit) # 77.0 temp.celsius = 30 # Uses the setter
Python 3.13class Cat: def speak(self): return "Meow!" class Dog: def speak(self): return "Woof!" class Duck: def speak(self): return "Quack!" # Same interface, different behavior animals = [Cat(), Dog(), Duck()] for animal in animals: print(animal.speak())
Python does not check the object's type — what matters is the presence of the required method:
Python 3.13class File: def read(self): return "data from a file" class Database: def read(self): return "data from a DB" def load_data(source): # The type doesn't matter, only that it has a 'read' method return source.read() print(load_data(File())) # data from a file print(load_data(Database())) # data from a DB
Python 3.13# len() works with different types print(len("Python")) # 6 print(len([1, 2, 3])) # 3 print(len({"a": 1})) # 1 # + behaves differently print(1 + 2) # 3 (addition) print("Hello, " + "world!") # Hello, world! (concatenation)
Receives a reference to the instance (self) as its first argument:
Python 3.13class MyClass: def instance_method(self): return f"Called for {self}"
Receives a reference to the class (cls) instead of an instance:
Python 3.13class User: def __init__(self, name, age): self.name = name self.age = age @classmethod def from_string(cls, data_string): name, age = data_string.split(",") return cls(name, int(age)) # Alternative constructor user = User.from_string("Anna,25") print(user.name) # Anna
Receives neither self nor cls. It behaves like a regular function placed inside a class namespace:
Python 3.13class MathUtils: @staticmethod def is_even(n): return n % 2 == 0 print(MathUtils.is_even(4)) # True
Python 3.13class Product: def __init__(self, name, price): self.name = name self.price = price def __str__(self): # For the end user (print, str()) return f"{self.name}: ${self.price}" def __repr__(self): # For the developer (debugging, repr()) return f"Product('{self.name}', {self.price})" p = Product("Book", 50) print(p) # Book: $50 print(repr(p)) # Product('Book', 50)
Python 3.13class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return self.x == other.x and self.y == other.y def __lt__(self, other): return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2) Point(1, 2) == Point(1, 2) # True Point(1, 2) < Point(3, 4) # True
Python 3.13class Basket: def __init__(self): self.items = [] def __len__(self): return len(self.items) def __contains__(self, item): return item in self.items basket = Basket() basket.items.append("apple") print(len(basket)) # 1 print("apple" in basket) # True
Python 3.13def log_call(func): def wrapper(*args, **kwargs): print(f"Calling function: {func.__name__}") result = func(*args, **kwargs) print(f"Result: {result}") return result return wrapper @log_call def add(a, b): return a + b add(3, 5) # Calling function: add # Result: 8
Python 3.13# Using @decorator syntax @log_call def add(a, b): return a + b # Is equivalent to: def add(a, b): return a + b add = log_call(add)
Python 3.13from functools import wraps def log_call(func): @wraps(func) # Keeps the original name and docstring def wrapper(*args, **kwargs): print(f"Calling: {func.__name__}") return func(*args, **kwargs) return wrapper @log_call def add(a, b): """Adds two numbers.""" return a + b print(add.__name__) # add (without @wraps it would be 'wrapper') print(add.__doc__) # Adds two numbers.
Python 3.13from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): """Calculate the area of the shape""" pass @abstractmethod def perimeter(self): """Calculate the perimeter of the shape""" pass # shape = Shape() # TypeError: Can't instantiate abstract class Create
Python 3.13class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return 2 * (self.width + self.height) class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): import math return math.pi * self.radius ** 2 def perimeter(self): import math return 2 * math.pi * self.radius rect = Rectangle(5, 3) print(rect.area()) # 15 print(rect.perimeter()) # 16
Python 3.13# Importing an entire module import math print(math.sqrt(16)) # 4.0 # Importing specific objects from math import sqrt, pi print(sqrt(16)) # 4.0 # Importing with an alias import datetime as dt now = dt.datetime.now()
Python 3.13# utils.py def greet(name): return f"Hello, {name}!" # main.py from utils import greet print(greet("World"))
Allows distinguishing whether the file is run directly or imported as a module:
Python 3.13# my_module.py def main(): print("Running the module directly") if __name__ == "__main__": # This code executes only when running directly: # python my_module.py main()
A package is a directory with an __init__.py file containing multiple modules:
my_package/
__init__.py
module_a.py
module_b.py
Python 3.13from my_package.module_a import some_function
# Create a virtual environment python -m venv venv # Activation # macOS/Linux: source venv/bin/activate # Windows: venv\Scripts\activate # Deactivation deactivate
# Install a package pip install requests # Install a specific version pip install requests==2.31.0 # List installed packages pip list # Save dependencies to a file pip freeze > requirements.txt # Install dependencies from a file pip install -r requirements.txt
requests==2.31.0
flask==3.0.0
pytest==7.4.3
This file locks all project dependencies, allowing you to recreate the environment on another machine.
Python 3.13import json data = { "name": "Anna", "age": 25, "hobbies": ["reading", "swimming"], "active": True } # To a string json_string = json.dumps(data, ensure_ascii=False, indent=2) print(json_string) # To a file with open("data.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)
Python 3.13# From a string json_string = '{"name": "Anna", "age": 25}' data = json.loads(json_string) print(data["name"]) # Anna # From a file with open("data.json", "r", encoding="utf-8") as f: data = json.load(f)
Python 3.13def greet(name: str) -> str: return f"Hello, {name}!" age: int = 25 price: float = 19.99 is_active: bool = True
Python 3.13# Python 3.9+ def process(items: list[str]) -> dict[str, int]: return {item: len(item) for item in items} # Nested types matrix: list[list[int]] = [[1, 2], [3, 4]]
Python 3.13from typing import Optional, Union # Can be str or None def find_user(user_id: int) -> Optional[str]: if user_id == 1: return "Anna" return None # Can be int or str def parse(value: Union[int, str]) -> str: return str(value)
Annotations are merely hints, not strict enforcements. Python does not enforce types at runtime:
Python 3.13def add(a: int, b: int) -> int: return a + b add("hello", " world") # Will run without error yielding: "hello world"
Creates a new object but does not copy nested objects — it maintains references to the originals:
Python 3.13import copy original = [[1, 2, 3], [4, 5, 6]] shallow = copy.copy(original) shallow[0][0] = 999 print(original[0][0]) # 999 — the original has changed too!
Creates a completely independent clone, including all nested objects:
Python 3.13import copy original = [[1, 2, 3], [4, 5, 6]] deep = copy.deepcopy(original) deep[0][0] = 999 print(original[0][0]) # 1 — the original remains unchanged
Python 3.13# For lists lst = [1, 2, 3] copy1 = lst.copy() copy2 = lst[:] copy3 = list(lst) # For dictionaries d = {"a": 1} copy4 = d.copy() copy5 = dict(d) # Universal import copy copy6 = copy.copy(lst)
Python 3.13# Shallow is sufficient nums = [1, 2, 3] # Elements are immutable ints safe_copy = nums.copy() # Deep copy is required matrix = [[1, 2], [3, 4]] # Nested lists safe_copy = copy.deepcopy(matrix)
Python 3.13# What 'for' does under the hood nums = [1, 2, 3] # for num in nums: # print(num) # Equivalent: iterator = iter(nums) # Calls nums.__iter__() while True: try: num = next(iterator) # Calls iterator.__next__() print(num) except StopIteration: break
Python 3.13class Countdown: def __init__(self, start): self.current = start def __iter__(self): return self def __next__(self): if self.current <= 0: raise StopIteration value = self.current self.current -= 1 return value for num in Countdown(5): print(num) # 5, 4, 3, 2, 1
The iteration protocol underpins for loops, generators, and many built-in functions. Custom iterators are useful when you need to process data in chunks — for example, reading a large file line by line instead of loading it all into memory.
Checks whether the values match between two objects:
Python 3.13a = [1, 2, 3] b = [1, 2, 3] print(a == b) # True — values are the same
Checks whether two variables point to the exact same object in memory:
Python 3.13a = [1, 2, 3] b = [1, 2, 3] print(a is b) # False — they are different objects print(id(a), id(b)) # Different memory addresses c = a print(a is c) # True — c points to the same object
Python caches integers from -5 to 256, so:
Python 3.13x = 100 y = 100 print(x is y) # True — cached object x = 1000 y = 1000 print(x is y) # False — different objects
Python 3.13# Correct if value is None: print("No value") # Incorrect if value == None: print("No value")
Every object keeps track of its reference count — the number of variables pointing to it. When the count reaches zero, the object is deleted:
Python 3.13import sys a = [1, 2, 3] print(sys.getrefcount(a)) # 2 (a + the function parameter itself) b = a # Another reference print(sys.getrefcount(a)) # 3 del b # Remove a reference print(sys.getrefcount(a)) # 2
Reference counting fails when there are circular references:
Python 3.13# Circular reference a = [] b = [] a.append(b) b.append(a) del a, b # The reference count won't reach zero, but the objects are unreachable
To solve this, Python runs a Garbage Collector (the gc module) that discovers and cleans up such cycles.
Python 3.13def make_multiplier(factor): def multiply(number): return number * factor # 'factor' is captured from the outer function return multiply double = make_multiplier(2) triple = make_multiplier(3) print(double(5)) # 10 print(triple(5)) # 15
Python 3.13# Counter def make_counter(start=0): count = start def counter(): nonlocal count count += 1 return count return counter c = make_counter() print(c()) # 1 print(c()) # 2 print(c()) # 3 # Logger def make_logger(prefix): def log(message): print(f"[{prefix}] {message}") return log error_log = make_logger("ERROR") error_log("File not found") # [ERROR] File not found
Threads operate within a single process and share the same memory space:
Python 3.13import threading def download(url): print(f"Downloading {url}") threads = [] for url in ["url1", "url2", "url3"]: t = threading.Thread(target=download, args=(url,)) threads.append(t) t.start() for t in threads: t.join() # Wait for all threads to finish
Each process gets its own memory space and its own Python interpreter:
Python 3.13from multiprocessing import Process def heavy_computation(n): return sum(i * i for i in range(n)) processes = [] for n in [10_000_000, 20_000_000]: p = Process(target=heavy_computation, args=(n,)) processes.append(p) p.start() for p in processes: p.join()
The GIL is a CPython mechanism that allows only one thread to execute Python bytecode at a time. As a result: