Beginner Python Interview Questions and Answers

๐Ÿ“‹ Table of Contents โ–พ
  1. Questions & Answers
  2. 📝 Knowledge Check

🐍 Beginner Python Interview Questions

This lesson covers the fundamental Python concepts every developer must know. Master data types, control flow, functions, list comprehensions, OOP basics, modules, exception handling, and the standard library. These questions reflect what interviewers ask at junior and entry-level Python roles.

Questions & Answers

01 What is Python? What are its key features?

Core Python is a high-level, interpreted, general-purpose programming language emphasising readability and simplicity. Created by Guido van Rossum in 1991, it follows the philosophy of “there should be one โ€” and preferably only one โ€” obvious way to do it.”

Key features:

  • Interpreted โ€” code runs line by line via the CPython interpreter; no explicit compilation step needed
  • Dynamically typed โ€” variable types are determined at runtime; no type declarations required
  • Strongly typed โ€” types are enforced; "5" + 5 raises a TypeError (unlike JavaScript)
  • Multi-paradigm โ€” supports procedural, object-oriented, and functional programming styles
  • Batteries included โ€” rich standard library covering file I/O, networking, JSON, regex, databases, testing, and more
  • Extensive ecosystem โ€” PyPI hosts 500,000+ packages: NumPy, Pandas, Django, Flask, FastAPI, PyTorch, TensorFlow
  • Indentation-based syntax โ€” code blocks are defined by indentation (4 spaces), not braces

Python 3 (current) is incompatible with Python 2 (EOL 2020). Always use Python 3.10+ for new projects.

02 What are Python’s built-in data types?

Data Types

# Numeric
x = 42          # int
y = 3.14        # float
z = 2 + 3j      # complex

# Text
s = "Hello"     # str (immutable sequence of Unicode characters)

# Boolean
b = True        # bool (subclass of int: True == 1, False == 0)

# None
n = None        # NoneType โ€” the absence of a value

# Sequences (ordered)
lst  = [1, 2, 3]        # list    โ€” mutable, allows duplicates
tpl  = (1, 2, 3)        # tuple   โ€” immutable, allows duplicates
rng  = range(0, 10, 2)  # range   โ€” lazy sequence of integers

# Mappings
dct = {"key": "value"}  # dict    โ€” mutable, key-value pairs (ordered in Python 3.7+)

# Sets (unordered, unique)
st   = {1, 2, 3}        # set     โ€” mutable, no duplicates
fst  = frozenset({1,2})  # frozenset โ€” immutable set

# Binary
ba   = bytes([65, 66])   # bytes      โ€” immutable byte sequence
byt  = bytearray([65])   # bytearray  โ€” mutable byte sequence
mv   = memoryview(b"hi") # memoryview โ€” memory-efficient view

Type checking: type(x) returns the exact type. isinstance(x, (int, float)) checks if x is an int or float (including subclasses). Prefer isinstance() in production code.

03 What is the difference between a list, tuple, and set?

Data Types

  • list โ€” ordered, mutable, allows duplicates. Use for sequences that change: shopping carts, task queues, logs.
  • tuple โ€” ordered, immutable, allows duplicates. Use for data that must not change: coordinates, RGB colours, function return values, dictionary keys. Slightly faster than lists.
  • set โ€” unordered, mutable, no duplicates. Use for membership testing, deduplication, and set operations. O(1) average lookup.
lst = [1, 2, 2, 3]    # list: ordered, duplicates OK
tpl = (1, 2, 2, 3)    # tuple: ordered, duplicates OK
st  = {1, 2, 2, 3}    # set: {1, 2, 3} โ€” duplicates removed

# List is mutable:
lst.append(4)     # [1, 2, 2, 3, 4]

# Tuple is immutable:
# tpl[0] = 99      # TypeError: 'tuple' object does not support item assignment

# Set operations:
a = {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 diff: {1, 2, 5, 6}

# Choosing:
# Need ordering + mutation?  โ†’ list
# Need ordering + immutable? โ†’ tuple
# Need fast membership test? โ†’ set  (1M items: 'x in set' is O(1) vs O(n) for list)

04 What is the difference between == and is in Python?

Core

  • == โ€” equality comparison. Checks if two objects have the same value. Calls __eq__().
  • is โ€” identity comparison. Checks if two variables point to the same object in memory (same id()). Equivalent to id(a) == id(b).
a = [1, 2, 3]
b = [1, 2, 3]
c = a

a == b   # True  โ€” same value
a is b   # False โ€” different objects in memory
a is c   # True  โ€” c is an ALIAS for a, same object

# Integer caching (CPython implementation detail)
x = 256
y = 256
x is y   # True  โ€” CPython caches small integers (-5 to 256)

x = 1000
y = 1000
x is y   # False โ€” large integers are not cached

# RULE: Use == to compare values. Use is ONLY for:
# - Checking for None:    if result is None
# - Checking for True/False:  if flag is True  (rare)
# - Checking identity explicitly

Common mistake: Using if x is "hello" instead of if x == "hello". String interning is an implementation detail โ€” never rely on it for equality.

05 What are Python’s mutable and immutable types? Why does it matter?

Core

  • Immutable โ€” value cannot be changed after creation: int, float, complex, bool, str, tuple, bytes, frozenset
  • Mutable โ€” value can be changed in place: list, dict, set, bytearray, user-defined class instances
# Immutable โ€” reassignment creates a NEW object
s = "hello"
id_before = id(s)
s += " world"
id(s) == id_before   # False โ€” new string object

# Mutable โ€” modification happens IN PLACE
lst = [1, 2, 3]
id_before = id(lst)
lst.append(4)
id(lst) == id_before  # True โ€” same object, mutated

# WHY IT MATTERS โ€” default mutable argument bug
def append_item(item, lst=[]):   # โŒ mutable default โ€” shared across calls!
    lst.append(item)
    return lst

append_item(1)  # [1]
append_item(2)  # [1, 2] โ€” NOT [2]!

# Fix: use None as default
def append_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

# Aliasing pitfall
a = [1, 2, 3]
b = a           # b is an alias โ€” same object!
b.append(4)
print(a)        # [1, 2, 3, 4] โ€” a was modified!
b = a.copy()    # โœ… shallow copy โ€” b is independent
import copy; b = copy.deepcopy(a)  # โœ… deep copy โ€” for nested objects

06 What are list comprehensions and generator expressions?

Comprehensions List comprehensions provide a concise way to create lists. They are more readable and often faster than equivalent for-loop code.

# Basic syntax: [expression for item in iterable if condition]

# Without comprehension
squares = []
for x in range(10):
    if x % 2 == 0:
        squares.append(x ** 2)

# With list comprehension
squares = [x ** 2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]

# Nested comprehension โ€” flatten a 2D list
matrix = [[1,2,3],[4,5,6],[7,8,9]]
flat = [n for row in matrix for n in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Dict comprehension
word_lengths = {word: len(word) for word in ["hello", "world"]}
# {"hello": 5, "world": 5}

# Set comprehension
unique_lengths = {len(word) for word in ["hello", "hi", "hey"]}
# {5, 2, 3}

# Generator expression โ€” lazy, memory-efficient (no list built in memory)
gen = (x ** 2 for x in range(1_000_000))  # uses parentheses, NOT brackets
next(gen)  # 0 โ€” computed on demand
sum(x ** 2 for x in range(1_000_000))     # no intermediate list created

Generator vs list: Use a generator when you iterate once and don’t need all values at once (large datasets, streaming). Use a list when you need to iterate multiple times or access items by index.

07 What are *args and **kwargs?

Functions

  • *args โ€” collects any number of positional arguments into a tuple
  • **kwargs โ€” collects any number of keyword arguments into a dict
def describe(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)

describe(1, 2, 3, name="Alice", role="admin")
# args: (1, 2, 3)
# kwargs: {'name': 'Alice', 'role': 'admin'}

# Mixing all parameter types (order matters)
def func(pos1, pos2, *args, keyword_only, **kwargs):
    pass

# Unpacking with * and **
def add(a, b, c):
    return a + b + c

nums = [1, 2, 3]
add(*nums)          # unpacks list โ†’ add(1, 2, 3)

config = {"a": 1, "b": 2, "c": 3}
add(**config)       # unpacks dict โ†’ add(a=1, b=2, c=3)

# Real-world use โ€” flexible wrapper function
def log_and_call(func, *args, **kwargs):
    print(f"Calling {func.__name__} with {args} {kwargs}")
    return func(*args, **kwargs)

log_and_call(add, 1, 2, c=3)

08 What are Python decorators? How do you write one?

Functions A decorator is a function that takes another function, adds some behaviour, and returns the modified function. The @decorator syntax is shorthand for func = decorator(func).

import functools, time

# Basic decorator
def timer(func):
    @functools.wraps(func)  # preserves func's __name__, __doc__ etc.
    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

@timer
def slow_function():
    time.sleep(0.1)

slow_function()  # slow_function took 0.1002s

# Decorator with arguments โ€” needs an extra layer
def retry(max_attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    print(f"Attempt {attempt} failed: {e}. Retrying...")
        return wrapper
    return decorator

@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url):
    ...  # may raise ConnectionError

09 What is the difference between deep copy and shallow copy?

Core

  • Assignment (b = a) โ€” creates an alias; both point to the same object. Changes via either name affect both.
  • Shallow copy โ€” creates a new container but the elements inside are still references to the same objects. Works for flat structures; nested mutables are still shared.
  • Deep copy โ€” creates a fully independent copy including all nested objects recursively. Always independent.
import copy

original = [[1, 2], [3, 4]]

# Shallow copy โ€” various ways
shallow1 = original.copy()
shallow2 = list(original)
shallow3 = original[:]

# Deep copy
deep = copy.deepcopy(original)

# Modify nested list
original[0].append(99)

print(original)  # [[1, 2, 99], [3, 4]]
print(shallow1)  # [[1, 2, 99], [3, 4]] โ€” inner list is shared!
print(deep)      # [[1, 2], [3, 4]]     โ€” completely independent

# When to use each:
# Shallow copy โ€” flat data structures, or when you want shared inner objects
# Deep copy    โ€” nested structures, when you need full independence
# copy.copy()  โ€” for custom objects with __copy__ method
# copy.deepcopy() โ€” handles cycles, custom __deepcopy__ method

10 What is OOP in Python? Explain classes, objects, and the four pillars.

OOP Object-Oriented Programming organises code around objects โ€” instances of classes that bundle data (attributes) and behaviour (methods).

class Animal:
    # Class attribute โ€” shared by all instances
    kingdom = "Animalia"

    def __init__(self, name, sound):   # constructor โ€” called on creation
        self.name  = name              # instance attribute
        self.sound = sound

    def speak(self):
        return f"{self.name} says {self.sound}"

    def __repr__(self):                # developer representation
        return f"Animal(name={self.name!r})"

    def __str__(self):                 # user-friendly string
        return self.name

class Dog(Animal):                     # Inheritance
    def __init__(self, name):
        super().__init__(name, "Woof")  # call parent constructor

    def speak(self):                    # Polymorphism (method override)
        return f"{super().speak()}!"

dog = Dog("Rex")
print(dog.speak())  # Rex says Woof!

Four pillars:

  • Encapsulation โ€” bundle data and methods, hide internals (_private, __name_mangled)
  • Inheritance โ€” child class inherits from parent (class Dog(Animal))
  • Polymorphism โ€” same method name, different implementations per class
  • Abstraction โ€” hide complex implementation, expose simple interface (abstract base classes)
11 What is the difference between @staticmethod and @classmethod?

OOP

  • Instance method โ€” takes self as first parameter. Has access to instance and class data. The default method type.
  • @classmethod โ€” takes cls (the class itself) as first parameter. Can access/modify class state. Often used as alternative constructors.
  • @staticmethod โ€” takes no implicit first parameter. Has no access to instance or class. A regular function scoped to the class for logical grouping.
class Temperature:
    unit = "Celsius"

    def __init__(self, value):
        self.value = value

    def display(self):                    # instance method
        return f"{self.value}ยฐ{self.unit}"

    @classmethod
    def from_fahrenheit(cls, f):          # alternative constructor
        return cls((f - 32) * 5 / 9)

    @classmethod
    def set_unit(cls, unit):              # modify class state
        cls.unit = unit

    @staticmethod
    def celsius_to_fahrenheit(c):         # utility โ€” no self or cls needed
        return c * 9 / 5 + 32

t1 = Temperature(100)
t2 = Temperature.from_fahrenheit(212)    # classmethod โ€” creates instance
Temperature.set_unit("C")               # classmethod โ€” modifies class
print(Temperature.celsius_to_fahrenheit(100))  # 212.0 โ€” staticmethod

12 What are Python’s dunder (magic) methods?

OOP Dunder (double underscore) methods let you define how objects behave with built-in operations, making your classes feel like first-class Python citizens.

class Vector:
    def __init__(self, x, y):        # constructor
        self.x, self.y = x, y

    def __repr__(self):              # repr(v) โ€” unambiguous for developers
        return f"Vector({self.x}, {self.y})"

    def __str__(self):               # str(v) โ€” user-friendly
        return f"({self.x}, {self.y})"

    def __add__(self, other):        # v1 + v2
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):       # v * 3
        return Vector(self.x * scalar, self.y * scalar)

    def __len__(self):               # len(v)
        return 2

    def __eq__(self, other):         # v1 == v2
        return self.x == other.x and self.y == other.y

    def __lt__(self, other):         # v1 < v2 โ€” for sorting
        return abs(self) < abs(other)

    def __abs__(self):               # abs(v)
        return (self.x**2 + self.y**2) ** 0.5

    def __bool__(self):              # bool(v) โ€” truth testing
        return bool(self.x or self.y)

    def __getitem__(self, idx):      # v[0], v[1]
        return (self.x, self.y)[idx]

    def __iter__(self):              # for component in v
        yield self.x; yield self.y

    def __contains__(self, val):     # val in v
        return val in (self.x, self.y)

13 What is exception handling in Python?

Exceptions

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print("Caught:", e)              # Caught: division by zero
except (TypeError, ValueError) as e: # multiple exceptions
    print("Type or Value error:", e)
except Exception as e:               # catch-all (avoid being too broad)
    raise                            # re-raise if you can't handle it
else:
    print("No exception โ€” runs only if try block succeeded")
finally:
    print("Always runs โ€” clean up resources here")

# Custom exceptions
class InsufficientFundsError(ValueError):
    def __init__(self, amount, balance):
        super().__init__(f"Cannot withdraw {amount}, balance is {balance}")
        self.amount = amount
        self.balance = balance

def withdraw(amount, balance):
    if amount > balance:
        raise InsufficientFundsError(amount, balance)
    return balance - amount

# Context manager for resource cleanup (preferred over try/finally for files)
with open("data.txt", "r") as f:
    content = f.read()  # file closed automatically even if exception occurs

# Exception chaining
try:
    int("abc")
except ValueError as e:
    raise RuntimeError("Failed to parse config") from e  # chained exception

14 What is a generator in Python? How does yield work?

Generators A generator is a function that uses yield to produce a sequence of values lazily โ€” one at a time, on demand. It maintains its state between calls, making it memory-efficient for large sequences.

# Regular function โ€” builds entire list in memory
def squares_list(n):
    return [x ** 2 for x in range(n)]

# Generator function โ€” yields one value at a time
def squares_gen(n):
    for x in range(n):
        yield x ** 2   # pauses here, returns x**2, resumes on next()

gen = squares_gen(5)
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 4
# Raises StopIteration when exhausted

# Iterating with for loop (handles StopIteration)
for sq in squares_gen(5):
    print(sq)   # 0, 1, 4, 9, 16

# Infinite generator โ€” no end, no memory issue
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(8)])  # [0, 1, 1, 2, 3, 5, 8, 13]

# send() โ€” communicate with generator
def accumulator():
    total = 0
    while True:
        value = yield total  # send() puts value here
        total += value

acc = accumulator()
next(acc)             # prime the generator
print(acc.send(10))   # 10
print(acc.send(20))   # 30

15 What are Python’s built-in functions? List the most important ones.

Built-ins

# Type conversion
int("42"), float("3.14"), str(42), bool(0), list((1,2)), tuple([1,2]), set([1,1,2])

# Math
abs(-5)          # 5
round(3.756, 2)  # 3.76
max([1,3,2])     # 3 โ€” also works with key: max(words, key=len)
min([1,3,2])     # 1
sum([1,2,3])     # 6
pow(2, 10)       # 1024  (same as 2 ** 10)
divmod(17, 5)    # (3, 2) โ€” quotient and remainder

# Iteration
len([1,2,3])          # 3
range(5)              # 0,1,2,3,4
enumerate(["a","b"])  # [(0,"a"),(1,"b")]
zip([1,2], ["a","b"]) # [(1,"a"),(2,"b")]
reversed([1,2,3])     # iterator: 3,2,1
sorted([3,1,2])       # [1,2,3]
sorted(data, key=lambda x: x["age"], reverse=True)

# Functional
map(str, [1,2,3])           # ["1","2","3"]
filter(None, [0,1,2,None])  # [1,2] โ€” removes falsy
any([False, True, False])   # True
all([True, True, False])    # False

# Object introspection
type(42)           # <class 'int'>
isinstance(42, int)  # True
dir(str)           # list of attributes and methods
hasattr(obj, "name")
getattr(obj, "name", "default")
id(obj)            # memory address

# I/O
print("hello", end="\n", sep=", ")
input("Enter value: ")

16 What are lambda functions in Python?

Functions A lambda is an anonymous, single-expression function defined with the lambda keyword. Used for short, throwaway functions โ€” primarily as arguments to sorted(), map(), filter().

# Lambda syntax: lambda parameters: expression
square = lambda x: x ** 2
add    = lambda x, y: x + y

square(5)     # 25
add(3, 4)     # 7

# Common uses
users = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
sorted_users = sorted(users, key=lambda u: u["age"])      # sort by age
oldest = max(users, key=lambda u: u["age"])               # find oldest

doubled = list(map(lambda x: x * 2, [1, 2, 3]))          # [2, 4, 6]
evens   = list(filter(lambda x: x % 2 == 0, range(10)))  # [0,2,4,6,8]

# Lambda limitations:
# - Single expression only (no statements, no assignments)
# - No docstrings
# - Can be harder to debug (no function name in tracebacks)

# When to prefer a named function over lambda:
# - Logic is complex (use def for readability)
# - Function is reused (give it a name)
# - You need to test it directly (named functions are easier to test)

17 What is the difference between range() and xrange()?

Core xrange() existed in Python 2 only. In Python 3, range() is the only range function and it works like Python 2’s xrange() โ€” it is a lazy object that generates numbers on demand, using O(1) memory regardless of size.

# Python 3 range() โ€” lazy, O(1) memory
r = range(0, 1_000_000)
print(type(r))     # <class 'range'>
print(r[500_000])  # 500000 โ€” random access in O(1)
print(len(r))      # 1000000
print(r[-1])       # 999999 โ€” negative indexing

# Range supports all sequence operations
r = range(0, 20, 2)
print(list(r))     # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
print(10 in r)     # True  โ€” O(1) check, not O(n)
print(r.count(10)) # 1
print(r.index(10)) # 5

# range() vs list() โ€” memory difference
import sys
print(sys.getsizeof(range(1_000_000)))     # 48 bytes regardless of size!
print(sys.getsizeof(list(range(1_000_000))))  # ~8.5 MB

# Use range() in for loops โ€” never convert to list unless you need a list
for i in range(10):   # lazy โ€” no list created
    print(i)

18 What are Python’s string methods and formatting options?

Strings

# String methods (strings are immutable โ€” methods return NEW strings)
s = "  Hello, World!  "
s.strip()           # "Hello, World!" โ€” removes whitespace
s.lower()           # "  hello, world!  "
s.upper()           # "  HELLO, WORLD!  "
s.replace("World", "Python")  # "  Hello, Python!  "
s.split(", ")       # ["  Hello", "World!  "]
", ".join(["a","b","c"])  # "a, b, c"
"hello world".title()     # "Hello World"
"hello".startswith("he")  # True
"hello".endswith("lo")    # True
"hello".find("ll")        # 2  (index) or -1 if not found
"abcabc".count("a")       # 2

# Formatting
name, age = "Alice", 30

# f-strings (Python 3.6+) โ€” PREFERRED
f"Hello, {name}! You are {age} years old."
f"{3.14159:.2f}"   # "3.14"
f"{1000000:,}"     # "1,000,000"
f"{'text':>10}"    # "      text" (right-align, width 10)
f"{42:08b}"        # "00101010" (binary, zero-padded to 8 chars)
f"{name=}"         # "name='Alice'" (self-documenting โ€” Python 3.8+)

# str.format()
"Hello, {}! You are {} years old.".format(name, age)
"Hello, {name}!".format(name=name)

# %-formatting (legacy, avoid)
"Hello, %s! You are %d years old." % (name, age)

19 What is the with statement and context managers?

Core The with statement ensures that setup and teardown code is always executed โ€” even if an exception occurs. It uses the context manager protocol: __enter__ (setup) and __exit__ (cleanup).

# File handling โ€” closes automatically on exit or exception
with open("data.txt", "r") as f:
    content = f.read()
# File is closed here even if an exception occurred inside

# Multiple context managers
with open("input.txt") as fin, open("output.txt", "w") as fout:
    fout.write(fin.read().upper())

# Custom context manager โ€” using a class
class Timer:
    import time
    def __enter__(self):
        self.start = self.time.perf_counter()
        return self  # becomes the 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = self.time.perf_counter() - self.start
        print(f"Elapsed: {self.elapsed:.4f}s")
        return False  # False = don't suppress exceptions

with Timer() as t:
    [x**2 for x in range(1_000_000)]

# Custom context manager โ€” using contextlib (simpler)
from contextlib import contextmanager

@contextmanager
def managed_resource():
    print("Setup")
    try:
        yield "the resource"   # 'with' block runs here
    finally:
        print("Teardown")

with managed_resource() as r:
    print(f"Using: {r}")

20 What are Python modules and packages? How do you import them?

Modules

  • Module โ€” a single Python file (.py). Contains functions, classes, and variables.
  • Package โ€” a directory of modules containing an __init__.py file. Organises related modules.
# Import styles
import math                       # import the whole module
from math import sqrt, pi         # import specific names
from math import sqrt as sq_root  # import with alias
import numpy as np                # alias for a package (convention)
from . import utils               # relative import (within same package)
from ..models import User         # relative import, parent package

# math module usage
math.sqrt(16)     # 4.0
math.pi           # 3.14159...
math.floor(3.7)   # 3
math.ceil(3.2)    # 4

# os and sys
import os
os.getcwd()                  # current directory
os.path.join("dir", "file")  # platform-safe path joining
os.environ.get("HOME")       # environment variable
os.listdir(".")              # directory contents

import sys
sys.path                     # list of directories Python searches for modules
sys.argv                     # command-line arguments
sys.version                  # Python version string
sys.exit(0)                  # exit the program

if __name__ == “__main__”: This block runs only when the file is executed directly, not when imported. Use it to separate library code from script code โ€” the standard pattern for any Python file that can be both imported and run.

21 What is pip and virtual environments in Python?

Tooling pip is Python’s package installer. Virtual environments are isolated Python environments โ€” each project gets its own set of packages, preventing version conflicts between projects.

# Create a virtual environment
python3 -m venv venv

# Activate
source venv/bin/activate          # Linux/Mac
venv\Scripts\activate             # Windows

# Deactivate
deactivate

# pip commands
pip install requests              # install a package
pip install requests==2.31.0      # install specific version
pip install "requests>=2.28"      # install with version constraint
pip install -r requirements.txt   # install from file
pip uninstall requests
pip list                          # list installed packages
pip freeze > requirements.txt     # export current packages

# Modern: uv (fast pip replacement written in Rust)
pip install uv
uv venv
uv pip install requests

# pyproject.toml (modern standard โ€” replaces setup.py)
[project]
name = "my-project"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = ["requests>=2.28", "fastapi>=0.100"]

[tool.uv]
dev-dependencies = ["pytest", "ruff", "mypy"]

22 What is the difference between a shallow copy of a dict and dict.update()?

Data Types

# dict.update() โ€” modifies the dict IN PLACE
d = {"a": 1, "b": 2}
d.update({"b": 99, "c": 3})
print(d)  # {"a": 1, "b": 99, "c": 3}  โ€” d is modified

# dict copy + update โ€” creates a new dict, leaves original unchanged
original = {"a": 1, "b": 2}
merged = {**original, "b": 99, "c": 3}   # dict unpacking (Python 3.5+)
print(original)  # {"a": 1, "b": 2}   โ€” unchanged
print(merged)    # {"a": 1, "b": 99, "c": 3}

# dict | operator (Python 3.9+) โ€” merge operator
merged = original | {"b": 99, "c": 3}   # new dict
original |= {"b": 99}                   # in-place merge

# Shallow copy of a dict
import copy
d1 = {"a": [1, 2], "b": 3}
d2 = d1.copy()          # shallow โ€” nested list is shared
d2["a"].append(99)
print(d1["a"])           # [1, 2, 99] โ€” shared inner list!

d3 = copy.deepcopy(d1)  # deep โ€” fully independent
d3["a"].append(0)
print(d1["a"])           # [1, 2, 99] โ€” unchanged

# dict methods summary
d = {"a": 1, "b": 2, "c": 3}
d.keys()      # dict_keys(["a","b","c"])
d.values()    # dict_values([1,2,3])
d.items()     # dict_items([("a",1),("b",2),("c",3)])
d.get("z", 0) # 0 โ€” default if missing (no KeyError)
d.pop("b")    # removes and returns 2
d.setdefault("d", []).append(4)  # set if missing, then use

📝 Knowledge Check

Test your understanding of Python fundamentals with these five questions.

🧠 Quiz Question 1 of 5

What is the output of: a = [1, 2, 3]; b = a; b.append(4); print(a)?





🧠 Quiz Question 2 of 5

Which of the following is TRUE about Python generators?





🧠 Quiz Question 3 of 5

What is the common pitfall of using a mutable default argument in a Python function?





🧠 Quiz Question 4 of 5

What does @functools.wraps(func) do inside a decorator?





🧠 Quiz Question 5 of 5

What is the difference between == and is when comparing two lists with the same values?