Python Interview Questions & Answers
Python interview questions covering data types, OOP, decorators, generators, asyncio, memory management, and common libraries — Basic to Advanced.
Ans: Python is a high-level, interpreted, dynamically typed, general-purpose programming language designed for readability and simplicity.
| Feature | Description |
|---|---|
| Interpreted | Code runs line-by-line without a compile step |
| Dynamically typed | Variable types are checked at runtime, not compile time |
| Garbage collected | Automatic memory management via reference counting + GC |
| Multi-paradigm | Supports OOP, functional, and procedural styles |
| Extensive stdlib | “Batteries included” — huge standard library out of the box |
| Cross-platform | Runs on Windows, macOS, Linux without modification |
Ans:
| Type | Ordered | Mutable | Duplicates | Key-Value | Syntax |
|---|---|---|---|---|---|
list | ✅ | ✅ | ✅ | ❌ | [1, 2, 3] |
tuple | ✅ | ❌ | ✅ | ❌ | (1, 2, 3) |
set | ❌ | ✅ | ❌ | ❌ | {1, 2, 3} |
dict | ✅ (3.7+) | ✅ | Keys: ❌ | ✅ | {"a": 1} |
my_list = [1, 2, 2, 3] # Ordered, mutable, allows duplicates
my_tuple = (1, 2, 2, 3) # Ordered, immutable
my_set = {1, 2, 3} # Unordered, no duplicates
my_dict = {"name": "Alice", "age": 30} # Key-value pairs
# When to use each:
# list → ordered sequence that changes (shopping cart)
# tuple → fixed data (coordinates, RGB values)
# set → membership testing, deduplication
# dict → lookup by key (configuration, JSON-like data)
Ans: Below is the difference between mutabke and immutable object:
- Mutable objects can be changed after creation.
- Immutable objects cannot.
| Mutable | Immutable |
|---|---|
list | int, float, bool |
dict | str |
set | tuple |
bytearray | frozenset, bytes |
# Mutable — same object modified in place
lst = [1, 2, 3]
print(id(lst)) # e.g., 140234567890
lst.append(4)
print(id(lst)) # SAME id — modified in place
# Immutable — new object created
s = "hello"
print(id(s)) # e.g., 140234567000
s = s + " world"
print(id(s)) # DIFFERENT id — new string object
# Gotcha: mutable default arguments
def add_item(item, lst=[]): # BAD — shared across calls!
lst.append(item)
return lst
def add_item(item, lst=None): # GOOD
if lst is None:
lst = []
lst.append(item)
return lst
Ans: A decorator is a function that wraps another function to extend its behaviour without modifying the original code. It implements the Decorator design pattern.
import functools
import time
# Basic decorator
def timer(func):
@functools.wraps(func) # Preserves original function metadata
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def slow_function(n):
"""Simulate work."""
return sum(range(n))
slow_function(1_000_000)
# Output: slow_function took 0.0234s
# Decorator with arguments
def retry(max_attempts=3, delay=1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5)
def call_external_api():
# Retries automatically on failure
...
Common built-in decorators:
@property— getter/setter on class attributes@staticmethod— method that doesn’t receiveself@classmethod— method that receives the class (cls) instead of instance@functools.lru_cache— memoization
Ans:
A generator is a function that uses yield to return values lazily — one at a time — instead of computing and returning all values at once.
# Regular function — loads all values into memory
def get_squares_list(n):
return [x**2 for x in range(n)] # Creates list of n items in memory
# Generator — yields one value at a time (lazy)
def get_squares_gen(n):
for x in range(n):
yield x**2
# Memory comparison
import sys
lst = get_squares_list(1_000_000)
gen = get_squares_gen(1_000_000)
print(sys.getsizeof(lst)) # ~8 MB
print(sys.getsizeof(gen)) # 112 bytes!
# Generator expressions (like list comprehensions but lazy)
squares = (x**2 for x in range(1000)) # Generator
squares_list = [x**2 for x in range(1000)] # List
# Infinite generator (impossible with a list)
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Use generators when:
- Processing large files line-by-line
- Producing an infinite sequence
- Building data processing pipelines
Ans:
*args— collects extra positional arguments into a tuple**kwargs— collects extra keyword arguments into a dict
def demo(*args, **kwargs):
print(f"args: {args}") # tuple
print(f"kwargs: {kwargs}") # dict
demo(1, 2, 3, name="Alice", role="DevOps")
# args: (1, 2, 3)
# kwargs: {'name': 'Alice', 'role': 'DevOps'}
# Passing to another function (forwarding)
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
# Unpacking with * and **
numbers = [1, 2, 3]
print(*numbers) # same as print(1, 2, 3)
config = {"sep": "-", "end": "\n"}
print(*numbers, **config) # 1-2-3
Correct order in function signature: def f(positional, *args, keyword_only, **kwargs)
Ans:
The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time — even on multi-core hardware.
Thread 1: ──[GIL acquired]──[run bytecode]──[GIL released]──waiting──
Thread 2: ──waiting────────────────────────[GIL acquired]──[run]──
Impact:
- CPU-bound tasks (number crunching) → threads don’t help; use
multiprocessinginstead - I/O-bound tasks (network, disk) → threads work fine; the GIL is released during I/O waits
import threading
import multiprocessing
# CPU-bound: multiprocessing wins (bypasses GIL — each process has its own GIL)
def cpu_work(n):
return sum(i**2 for i in range(n))
# I/O-bound: threading works fine (GIL released during sleep/network/file I/O)
import requests
import concurrent.futures
urls = ["https://api.example.com/data"] * 10
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as pool:
responses = list(pool.map(requests.get, urls))
# Python 3.13+ introduces a "free-threaded" mode (experimental)
# that removes the GIL — but this is still stabilizing.
# Current best practice:
# CPU-bound → multiprocessing.Pool
# I/O-bound → asyncio or threading
# Mixed → asyncio + run_in_executor for CPU parts
Ans: A shallow copy creates a new outer object but shares references to nested objects. A deep copy recursively copies all nested objects, making the copy completely independent.
import copy
original = {"name": "Alice", "scores": [90, 85, 78]}
# Shallow copy — copies the outer object, inner objects are shared
shallow = copy.copy(original)
shallow["name"] = "Bob" # Does NOT affect original
shallow["scores"].append(100) # DOES affect original (same list object!)
print(original["name"]) # Alice (unchanged)
print(original["scores"]) # [90, 85, 78, 100] ← affected!
# Deep copy — recursively copies all nested objects
original = {"name": "Alice", "scores": [90, 85, 78]}
deep = copy.deepcopy(original)
deep["scores"].append(100)
print(original["scores"]) # [90, 85, 78] ← NOT affected
| Outer Object | Nested Objects | |
|---|---|---|
Assignment (=) | Same reference | Same reference |
copy.copy() (shallow) | New object | Same reference |
copy.deepcopy() (deep) | New object | New objects (recursive) |
Ans:
A context manager guarantees cleanup code runs even if an exception occurs. It implements __enter__ and __exit__ dunder methods.
# Without context manager (fragile)
f = open("file.txt", "r")
try:
data = f.read()
finally:
f.close() # Must manually close — could be missed
# With context manager (safe)
with open("file.txt", "r") as f:
data = f.read()
# f.close() called automatically, even on exception
# Creating your own context manager (class-based)
class DatabaseConnection:
def __enter__(self):
self.conn = connect_to_db()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
self.conn.close()
return False # Don't suppress exceptions
with DatabaseConnection() as db:
db.execute("SELECT 1")
# Creating context managers with @contextmanager (simpler)
from contextlib import contextmanager
@contextmanager
def managed_resource():
resource = acquire_resource()
try:
yield resource # Code inside 'with' block runs here
finally:
release_resource(resource) # Always runs
with managed_resource() as r:
r.do_something()
Ans:
# List comprehension — creates a full list in memory
squares = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
# Dict comprehension
word_lengths = {word: len(word) for word in ["aws", "kubernetes", "docker"]}
# {'aws': 3, 'kubernetes': 10, 'docker': 6}
# Set comprehension
unique_lengths = {len(word) for word in ["cat", "bat", "door", "mat"]}
# {3, 4}
# Generator expression (lazy — use () not [])
squares_gen = (x**2 for x in range(10_000_000))
# No memory allocated yet — computed on demand
# Rule of thumb:
# List comprehension → need all results at once, or reuse multiple times
# Generator expression → single-pass consumption, large/infinite datasets
# Generator expressions compose well (pipeline)
lines = (line.strip() for line in open("huge.log"))
errors = (line for line in lines if "ERROR" in line)
first_10 = list(itertools.islice(errors, 10))
# Memory: only one line loaded at a time
Ans:
| Method | Purpose | Called by | Audience |
|---|---|---|---|
__repr__ | Unambiguous, developer representation | repr(), REPL | Developers / debugging |
__str__ | Human-readable, user-friendly | print(), str() | End users |
from datetime import datetime
dt = datetime.now()
print(repr(dt)) # datetime.datetime(2024, 1, 15, 14, 30, 45, 123456)
print(str(dt)) # 2024-01-15 14:30:45.123456
class ServerConfig:
def __init__(self, host, port):
self.host = host
self.port = port
def __repr__(self):
# Should be unambiguous — ideally recreatable
return f"ServerConfig(host='{self.host}', port={self.port})"
def __str__(self):
# Human-friendly
return f"{self.host}:{self.port}"
cfg = ServerConfig("localhost", 8080)
print(repr(cfg)) # ServerConfig(host='localhost', port=8080)
print(str(cfg)) # localhost:8080
print(cfg) # localhost:8080 (uses __str__)
Rule: Always define __repr__. Define __str__ only when the user-facing format differs.
Ans:
class Order:
TAX_RATE = 0.08
def __init__(self, amount):
self.amount = amount
def total(self):
"""Instance method — has access to self and the instance."""
return self.amount * (1 + self.TAX_RATE)
@classmethod
def from_dict(cls, data: dict):
"""Class method — has access to cls (the class itself, not the instance).
Useful as alternative constructors."""
return cls(data["amount"])
@staticmethod
def is_valid_amount(amount: float) -> bool:
"""Static method — no access to self or cls.
A utility function that logically belongs to the class."""
return amount > 0
# Usage
order = Order(100)
order2 = Order.from_dict({"amount": 200}) # classmethod
print(Order.is_valid_amount(-5)) # False — staticmethod
| Receives | Access to | Use case | |
|---|---|---|---|
| Instance method | self | Instance + class | Most methods |
@classmethod | cls | Class only | Alternative constructors |
@staticmethod | Nothing | Nothing | Utility functions |
Ans:
Python uses two main mechanisms:
1. Reference Counting — Every object tracks how many references point to it. When the count drops to 0, memory is freed immediately.
import sys
x = [1, 2, 3]
print(sys.getrefcount(x)) # 2 (x + the getrefcount argument)
y = x # Reference count becomes 3
del x # Reference count becomes 2
y = None # Reference count becomes 0 → object freed
2. Cyclic Garbage Collector — Reference counting cannot handle circular references:
# Circular reference — neither object reaches refcount 0
a = {}
b = {}
a["other"] = b
b["other"] = a
# Python's gc module detects and cleans these up
import gc
gc.collect() # Manually trigger cycle detection
Memory optimization tools:
# __slots__ — prevents __dict__ creation, saves 40-50% memory per instance
class Point:
__slots__ = ["x", "y"] # No __dict__, no arbitrary attributes
def __init__(self, x, y):
self.x = x
self.y = y
# For millions of objects, this matters:
import sys
class PointDict:
def __init__(self, x, y): self.x = x; self.y = y
class PointSlots:
__slots__ = ["x", "y"]
def __init__(self, x, y): self.x = x; self.y = y
print(sys.getsizeof(PointDict(1,2))) # ~48 bytes + dict overhead
print(sys.getsizeof(PointSlots(1,2))) # ~56 bytes total (no dict)
Ans:
asyncio is Python’s built-in library for asynchronous I/O using a single-threaded event loop. It uses async/await syntax to write non-blocking code.
import asyncio
import aiohttp # pip install aiohttp
# WITHOUT asyncio — sequential (slow)
# Each request waits for the previous one to complete
# 10 requests × 200ms each = 2000ms total
# WITH asyncio — concurrent (fast)
# All requests fire "simultaneously" — 10 requests ≈ 200ms total
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks) # Run all concurrently
return results
urls = ["https://api.example.com/user/1",
"https://api.example.com/user/2",
"https://api.example.com/user/3"]
results = asyncio.run(fetch_all(urls))
# asyncio vs threading vs multiprocessing
# I/O-bound, many connections → asyncio (best — low overhead)
# I/O-bound, simple → threading (easier)
# CPU-bound computation → multiprocessing (bypasses GIL)
Key concepts:
async def→ defines a coroutine (doesn’t run until awaited)await→ suspends the current coroutine, yields control to event loopasyncio.gather()→ runs multiple coroutines concurrentlyasyncio.run()→ entry point — starts the event loop
Ans:
Type hints are annotations that declare the expected types of variables, function parameters, and return values. They don’t affect runtime but enable static analysis tools.
from typing import Optional, List, Dict, Union, Tuple
from dataclasses import dataclass
# Basic type hints
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
# Complex types
def process_users(users: List[Dict[str, str]]) -> Dict[str, int]:
return {u["name"]: len(u["email"]) for u in users}
# Optional (can be None)
def find_user(user_id: int) -> Optional[str]:
return db.get(user_id) # Returns str or None
# Python 3.10+ — union with |
def parse_value(val: str | int | None) -> float:
return float(val) if val is not None else 0.0
# dataclass — combines type hints with automatic __init__, __repr__
@dataclass
class ServiceConfig:
host: str
port: int
timeout: float = 30.0
tags: List[str] = None
def __post_init__(self):
if self.tags is None:
self.tags = []
cfg = ServiceConfig(host="localhost", port=8080)
print(cfg) # ServiceConfig(host='localhost', port=8080, timeout=30.0, tags=[])
Benefits:
- IDEs provide better autocomplete and error detection
- Tools like
mypycatch type errors before runtime - Code is self-documenting
Ans:
==checks value equality (do they hold the same data?)ischecks identity equality (are they the exact same object in memory?)
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True — same values
print(a is b) # False — different objects
print(a is c) # True — same object (c points to a)
# Integer caching — CPython caches small integers (-5 to 256)
x = 256
y = 256
print(x is y) # True — same cached object
x = 257
y = 257
print(x is y) # False — outside cache range, different objects
# Correct usage of `is`
if response is None: # ✅ Correct — None is a singleton
handle_empty()
if response == None: # ⚠️ Works but less Pythonic
handle_empty()
Rule: Use is only for None, True, and False (singletons). Use == for everything else.
Ans:
import logging
def read_config(path: str) -> dict:
try:
# Code that may raise an exception
with open(path) as f:
return json.load(f)
except FileNotFoundError:
# Specific exception — file doesn't exist
logging.error(f"Config file not found: {path}")
return {}
except json.JSONDecodeError as e:
# Another specific exception — bad JSON
logging.error(f"Invalid JSON in {path}: {e}")
raise ValueError(f"Config file is malformed: {path}") from e
except (PermissionError, OSError) as e:
# Multiple exception types in one clause
raise RuntimeError(f"Cannot read config: {e}") from e
else:
# Runs ONLY if no exception was raised in try
logging.info(f"Config loaded successfully from {path}")
finally:
# ALWAYS runs — cleanup code (even if exception propagates)
logging.debug("read_config complete")
# Custom exceptions
class InsufficientFundsError(ValueError):
def __init__(self, balance: float, amount: float):
self.balance = balance
self.amount = amount
super().__init__(f"Cannot withdraw {amount}: balance is {balance}")
def withdraw(balance: float, amount: float) -> float:
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
Ans:
from abc import ABC, abstractmethod
# ABSTRACTION — abstract base class defines interface, not implementation
class NotificationService(ABC):
@abstractmethod
def send(self, message: str, recipient: str) -> bool:
"""Must be implemented by subclasses."""
...
def send_with_retry(self, message, recipient, max_retries=3):
"""Concrete method available to all subclasses."""
for attempt in range(max_retries):
if self.send(message, recipient):
return True
return False
# INHERITANCE — subclass extends/overrides base class
class EmailService(NotificationService):
def __init__(self, smtp_host: str):
self._smtp_host = smtp_host # ENCAPSULATION — _ prefix = private by convention
def send(self, message: str, recipient: str) -> bool:
# Concrete implementation
print(f"Email to {recipient}: {message}")
return True
class SlackService(NotificationService):
def __init__(self, webhook_url: str):
self.__webhook = webhook_url # __ name mangling = stronger encapsulation
def send(self, message: str, recipient: str) -> bool:
print(f"Slack to #{recipient}: {message}")
return True
# POLYMORPHISM — same interface, different behaviour
services: list[NotificationService] = [
EmailService("smtp.gmail.com"),
SlackService("https://hooks.slack.com/..."),
]
alert = "Deployment complete!"
for service in services:
service.send(alert, "devops-team") # Calls the right implementation automatically
Ans:
A lambda is an anonymous single-expression function. It’s syntactic sugar for a simple function.
# Regular function
def double(x):
return x * 2
# Equivalent lambda
double = lambda x: x * 2
# Lambdas shine as inline callbacks
numbers = [5, 2, 8, 1, 9, 3]
numbers.sort(key=lambda x: -x) # Sort descending
# [9, 8, 5, 3, 2, 1]
users = [{"name": "Charlie", "age": 30}, {"name": "Alice", "age": 25}]
users.sort(key=lambda u: u["name"]) # Sort by name
# With filter() and map()
evens = list(filter(lambda x: x % 2 == 0, range(10))) # [0, 2, 4, 6, 8]
squares = list(map(lambda x: x**2, [1, 2, 3, 4])) # [1, 4, 9, 16]
When NOT to use lambdas:
- When the logic is complex (use a named function for readability)
- When you need docstrings or type hints
- When you’ll reuse the function (assign to a variable instead)
Ans:
@property turns a method into a read-only attribute with getter/setter/deleter control.
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius # Internal storage
@property
def celsius(self) -> float:
"""Getter."""
return self._celsius
@celsius.setter
def celsius(self, value: float):
"""Setter — validates before storing."""
if value < -273.15:
raise ValueError(f"Temperature below absolute zero: {value}")
self._celsius = value
@celsius.deleter
def celsius(self):
del self._celsius
@property
def fahrenheit(self) -> float:
"""Computed property — no setter needed."""
return self._celsius * 9/5 + 32
t = Temperature(100)
print(t.fahrenheit) # 212.0 — looks like an attribute, runs code
t.celsius = -274 # Raises ValueError
t.celsius = 0
print(t.fahrenheit) # 32.0
Ans:
A virtual environment is an isolated Python environment with its own interpreter and packages — separate from the system Python.
# Create virtual environment
python -m venv venv
# Activate (Linux/macOS)
source venv/bin/activate
# Activate (Windows)
venv\Scripts\activate
# Install packages (only in this venv, not globally)
pip install fastapi uvicorn
# Freeze dependencies
pip freeze > requirements.txt
# Deactivate
deactivate
Why they matter:
- Dependency isolation — Project A needs Django 3.x, Project B needs Django 4.x — both work on the same machine
- Reproducibility —
requirements.txt+ venv ensures everyone runs the same versions - No root access required — install packages without sudo
# Modern tooling
# uv — ultra-fast alternative to pip + venv (recommended)
pip install uv
uv venv
uv pip install fastapi
# poetry — dependency management + virtual envs
poetry init
poetry add fastapi
poetry shell
Ans:
A package is a directory containing Python modules and an __init__.py file. The __init__.py marks the directory as a package and can initialize shared state.
myapp/
├── __init__.py # Makes myapp a package
├── config.py
├── models/
│ ├── __init__.py # Makes models a sub-package
│ ├── user.py
│ └── order.py
└── services/
├── __init__.py
└── payment.py
# myapp/__init__.py — expose public API
from .config import settings
from .models.user import User
__version__ = "1.0.0"
__all__ = ["User", "settings"] # Controls what 'from myapp import *' exports
# Usage
from myapp import User # Works because of __init__.py
from myapp.models.order import Order # Direct import also works
In Python 3.3+, __init__.py is optional for “namespace packages”, but best practice is to include it for regular packages to be explicit.
Ans:
# Reading a file
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read() # Entire file as string
# OR
lines = f.readlines() # List of lines
# OR (memory efficient for large files)
for line in f: # Iterator — one line at a time
process(line.strip())
# Writing a file
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Hello, World!\n")
f.writelines(["line1\n", "line2\n"])
# Appending
with open("log.txt", "a") as f:
f.write("New log entry\n")
# Binary mode — for images, PDFs, executables
with open("image.png", "rb") as f:
data = f.read() # bytes, not str
with open("copy.png", "wb") as f:
f.write(data)
| Mode | Description |
|---|---|
"r" | Read text (default) |
"w" | Write text (overwrites) |
"a" | Append text |
"rb" | Read binary |
"wb" | Write binary |
"r+" | Read and write |
Ans:
import threading
import multiprocessing
import time
# I/O-bound task — threading is fine (GIL released during I/O)
def download_file(url):
import requests
return requests.get(url).content
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(download_file, urls))
# CPU-bound task — multiprocessing bypasses GIL
def compute_intensive(n):
return sum(i**2 for i in range(n))
with multiprocessing.Pool(processes=4) as pool:
results = pool.map(compute_intensive, [10**7] * 8)
| Feature | threading | multiprocessing |
|---|---|---|
| Parallelism | Concurrent (GIL limits true parallel) | True parallel (separate processes) |
| Memory | Shared memory | Separate memory space |
| Communication | Shared objects (with locks) | Queues, Pipes, shared memory |
| Overhead | Low | Higher (process startup) |
| Best for | I/O-bound | CPU-bound |
| GIL affected | Yes | No |
# ProcessPoolExecutor — modern way to use multiprocessing
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(compute_intensive, [10**7] * 8))
Ans:
from collections import Counter, defaultdict, OrderedDict, deque
# Counter — count occurrences
words = ["aws", "docker", "aws", "kubernetes", "docker", "aws"]
count = Counter(words)
print(count) # Counter({'aws': 3, 'docker': 2, 'kubernetes': 1})
print(count.most_common(2)) # [('aws', 3), ('docker', 2)]
count.update(["aws", "helm"]) # Add more counts
# defaultdict — dict with default value for missing keys (no KeyError)
graph = defaultdict(list)
graph["A"].append("B") # No need to check if "A" exists
graph["A"].append("C")
print(graph) # defaultdict(<class 'list'>, {'A': ['B', 'C']})
# Versus regular dict:
try:
regular = {}
regular["A"].append("B") # KeyError!
except KeyError:
regular["A"] = ["B"] # Must manually initialize
# deque — double-ended queue with O(1) append/pop from both ends
queue = deque(maxlen=5) # Fixed-size sliding window
for i in range(10):
queue.append(i) # Auto-evicts oldest when full
print(queue) # deque([5, 6, 7, 8, 9], maxlen=5)
queue.appendleft(99) # Add to left
queue.rotate(2) # Rotate elements
Add More Questions to This Guide
Know a question that should be here? Share it and help the community!
Open Google Form