Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Python Backend Developer Top 100 Interview Questions

100 Python backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the language core, concurrency and asyncio, web request handling, databases, caching, testing, security, performance and system design, from the basics to production scenarios.

100 questions

Questions 61-80 of 100

  1. Q61.Why should a database connection not be shared across threads or async tasks?

    A connection holds a single conversation with the database, including the current transaction. If two threads or tasks use it at once, their statements interleave inside the same transaction, so one request's rollback can undo another's work, and results can arrive at the wrong caller.

    • Thread safety varies: the database interface standard lets each driver declare how thread-safe it is, and many allow sharing only the module, not connections.
    • Async drivers expect one operation at a time per connection; overlapping awaits on one connection raise errors or corrupt the protocol state.
    • Transactions are per connection, so isolation between requests depends on each request using its own.

    The right pattern:

    1. Use a connection pool, sized to the database's capacity.
    2. Each request or unit of work borrows a connection, uses it for its transaction, and returns it.
    3. Return connections in a finally block or context manager, so errors never leak them.

    Watch out for objects that hide a connection inside, such as an ORM session stored at module level. They need the same per-request scoping.

  2. Q62.How do you manage database schema migrations in a Python project?

    Keep every schema change as a small, versioned migration script in the repository, apply them in order through the deployment pipeline, and record which ones each database has already run. The schema then evolves through reviewed code, never through manual changes.

    flowchart LR
      A["Write migration script"] --> B["Code review"]
      B --> C["Run on a copy of production data"]
      C --> D["Apply in pipeline before deploy"]
      D --> E["Deploy compatible code"]
      E --> F["Later migration removes old columns"]

    Practices that prevent incidents:

    1. One change per migration, with a clear name, so problems are easy to locate and revert.
    2. Review generated migrations: auto-generated scripts can drop and recreate a column instead of renaming it, which deletes data.
    3. Backward compatible first: add new columns and tables before code uses them, and remove old ones only after no deployed code needs them.
    4. Separate data backfills from schema changes, and run large ones in batches.
    5. Check lock behavior: adding an index or a column with a default can lock a large table on some databases.
    6. Test on realistic data volume before production.
  3. Q63.How do you serialize datetimes, decimals and custom objects to JSON in Python?

    The standard json module only knows basic types, so you tell it how to convert the rest. Pass a default function, or subclass json.JSONEncoder, that turns each unsupported object into a JSON-friendly value.

    • Datetimes: send standard date-time strings with an explicit offset, such as 2026-03-01T09:30:00+00:00, so clients never guess the time zone.
    • Decimals: send them as strings to keep exact precision; converting to float silently loses cents on some values.
    • Dataclasses: convert with dataclasses.asdict, then let the default function handle the field types inside.
    • Unknown types: raise a TypeError rather than calling str() on everything, which hides mistakes.

    On the way in, parse these values back explicitly at the boundary, for example with datetime.fromisoformat and Decimal, and reject invalid formats with a validation error.

    For heavy traffic, serialization can be a noticeable share of CPU time, so measure it on large responses and keep payloads lean.

    Example

    An invoice with a decimal amount and an aware datetime is converted to JSON through a default function.

    import json
    from dataclasses import asdict, dataclass
    from datetime import datetime, timezone
    from decimal import Decimal
    
    @dataclass
    class Invoice:
        number: str
        amount: Decimal
        issued_at: datetime
    
    def to_json(value):
        if isinstance(value, Decimal):
            return str(value)
        if isinstance(value, datetime):
            return value.isoformat()
        raise TypeError(f"cannot serialize {type(value).__name__}")
    
    invoice = Invoice("INV-7", Decimal("1499.50"), datetime(2026, 3, 1, 9, 30, tzinfo=timezone.utc))
    print(json.dumps(asdict(invoice), default=to_json))  # {"number": "INV-7", "amount": "1499.50", "issued_at": "2026-03-01T09:30:00+00:00"}
  4. Q64.Why should money use `Decimal` instead of `float` in Python?

    Floats store numbers in binary, and most decimal fractions such as 0.1 have no exact binary form. Small errors appear in sums and comparisons, and over many transactions they turn into visible mistakes in totals.

    • Decimal is exact for decimal values created from strings or integers, so Decimal("0.1") + Decimal("0.2") is exactly 0.3.
    • Create it from strings, never from floats: Decimal(0.1) faithfully copies the float's binary error.
    • Round explicitly with quantize and a named rounding mode, at the points your business rules specify, such as per line item or per invoice.
    • Store it precisely: use a fixed-precision numeric column in the database, or store integer minor units such as paise or cents.

    Alternatives: integers of minor units are fast and simple for a single currency. Decimal is clearer when prices, tax rates and rounding rules involve fractions.

    Never compare money floats with ==, and keep the currency with every amount, since adding rupees to dollars is a bug no number type can catch.

    Example

    Float addition drifts, while Decimal gives exact results and rounds tax with a stated rule.

    from decimal import Decimal, ROUND_HALF_UP
    
    print(0.1 + 0.2)  # 0.30000000000000004
    print(Decimal("0.1") + Decimal("0.2"))  # 0.3
    
    price = Decimal("19.99")
    tax = (price * Decimal("0.18")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    print(tax)  # 3.60
    
    print(Decimal(0.1))  # 0.1000000000000000055511151231257827021181583404541015625
  5. Q65.How do you handle time zones correctly in a Python backend?

    Store and compute with aware datetimes in UTC, convert to the user's time zone only when displaying or interpreting local times, and never mix naive and aware values.

    • Naive versus aware: a naive datetime has no time zone, so its meaning depends on assumptions; an aware one carries an offset or zone.
    • Get the current time correctly: datetime.now(timezone.utc) returns an aware UTC value; datetime.utcnow() returns a naive one and is deprecated in recent versions.
    • Named zones: use the standard zoneinfo module for zones with daylight-saving rules; it needs time zone data, which some systems provide through a separate package.
    • Store UTC in the database with a type that keeps the offset, or document clearly that a column holds UTC.
    • Local business rules: "orders placed today" depends on the business's zone, so convert before taking the date.

    Classic bugs: comparing naive and aware values raises a TypeError, and grouping UTC timestamps by date shifts late-evening orders into the wrong day for users east or west of UTC.

    Example

    A UTC timestamp falls on the next calendar day in India, and comparing a naive value with an aware one fails.

    from datetime import datetime, timedelta, timezone
    
    IST = timezone(timedelta(hours=5, minutes=30), "IST")
    stored = datetime(2026, 3, 1, 18, 45, tzinfo=timezone.utc)
    local = stored.astimezone(IST)
    
    print(local.isoformat())  # 2026-03-02T00:15:00+05:30
    print(local.date())  # 2026-03-02
    
    naive = datetime(2026, 3, 1, 18, 45)
    try:
        naive < stored
    except TypeError:
        print("cannot compare naive and aware datetimes")  # cannot compare naive and aware datetimes
  6. Q66.How do you cache results in a Python service, from memoization to a shared cache?

    Cache at the layer that removes the most repeated work, and decide up front how stale the data may be and how it will be invalidated. Caching without an invalidation plan turns performance problems into correctness problems.

    Function memoizationShared cache serviceHTTP caching
    Stores results in the process's own memoryStores results in a separate server that every instance usesLets browsers and proxies reuse whole responses
    Fastest, but each worker keeps its own copyOne shared copy, at the cost of a network callRemoves requests entirely, but only for cacheable responses
    Hard to invalidate across workersOne delete invalidates it for every instanceControlled by response headers and expiry times

    When to use which:

    • Use functools.lru_cache or a small in-process cache for pure functions and reference data that rarely changes.
    • Use a shared cache for data that must look the same on every instance, such as sessions or expensive query results.
    • Use HTTP caching for public, identical responses such as product listings.

    Rules that keep caches correct: include every input that changes the result in the key, including user and permissions; set a maximum size and a time to live; and protect popular keys from stampedes when they expire.

  7. Q67.How would you implement an LRU cache with a fixed capacity?

    Keep entries in insertion order, move an entry to the most-recent end every time it is read or written, and remove the least recently used entry from the other end when the cache is full. collections.OrderedDict provides both operations in constant time.

    • Get: if the key exists, move it to the end and return its value; otherwise return a default.
    • Put: store the value, move the key to the end, and if the size exceeds the capacity, pop the first item.
    • Validation: reject a capacity of zero or less, because such a cache could never hold anything.

    Production considerations:

    • Wrap operations in a lock if several threads share the cache, since a read also changes the order.
    • Add an expiry time per entry when the underlying data can change.
    • For pure functions, the built-in functools.lru_cache already does this and reports hits and misses through cache_info().

    This is a classic interview exercise because it combines a hash table for lookup with an ordering structure for eviction.

    Example

    After reading a, inserting c evicts b, the least recently used key.

    from collections import OrderedDict
    
    class LRUCache:
        def __init__(self, capacity):
            if capacity <= 0:
                raise ValueError("capacity must be positive")
            self.capacity = capacity
            self.items = OrderedDict()
    
        def get(self, key, default=None):
            if key not in self.items:
                return default
            self.items.move_to_end(key)
            return self.items[key]
    
        def put(self, key, value):
            self.items[key] = value
            self.items.move_to_end(key)
            if len(self.items) > self.capacity:
                self.items.popitem(last=False)
    
    cache = LRUCache(2)
    cache.put("a", 1)
    cache.put("b", 2)
    cache.get("a")
    cache.put("c", 3)
    
    print(list(cache.items))  # ['a', 'c']
    print(cache.get("b"))  # None
  8. Q68.How do you implement cursor-based pagination with an opaque cursor in Python?

    Sort by a stable order with a unique tie-breaker, return a page, and give the client a cursor that encodes the sort values of the last row. The next request asks for rows after those values, which the database can find with an index instead of skipping rows.

    1. Stable order: sort by the business field plus the primary key, such as newest first and then highest ID, so ties never reorder between pages.
    2. Cursor contents: encode the last row's sort values as JSON, then Base64, so clients treat it as an opaque token.
    3. Next page query: use a condition that means "strictly after the last row" in that order.
    4. End of data: return no cursor when the page is shorter than the limit.

    Practical points:

    • Validate and decode the cursor defensively, returning a 400 status for a malformed one.
    • The cursor is not secret; sign it if clients must not be able to change its contents, and never put permissions in it.
    • Add an index on the sort columns, or each page becomes a full scan anyway.

    Example

    Seven jobs are read three at a time, newest first, with ties broken by ID.

    import base64
    import json
    import sqlite3
    
    db = sqlite3.connect(":memory:")
    db.execute("CREATE TABLE jobs (id INTEGER PRIMARY KEY, posted INTEGER)")
    db.executemany("INSERT INTO jobs VALUES (?, ?)", [(i, 100 - i // 2) for i in range(1, 8)])
    
    def encode(posted, job_id):
        return base64.urlsafe_b64encode(json.dumps([posted, job_id]).encode()).decode()
    
    def decode(cursor):
        return json.loads(base64.urlsafe_b64decode(cursor))
    
    def page(limit, cursor=None):
        if cursor is None:
            rows = db.execute("SELECT posted, id FROM jobs ORDER BY posted DESC, id DESC LIMIT ?", (limit,)).fetchall()
        else:
            posted, job_id = decode(cursor)
            rows = db.execute(
                "SELECT posted, id FROM jobs WHERE posted < ? OR (posted = ? AND id < ?) "
                "ORDER BY posted DESC, id DESC LIMIT ?",
                (posted, posted, job_id, limit),
            ).fetchall()
        next_cursor = encode(*rows[-1]) if len(rows) == limit else None
        return [job_id for _, job_id in rows], next_cursor
    
    cursor = None
    while True:
        ids, cursor = page(3, cursor)
        print(ids)
        if cursor is None:
            break
    # Output:
    # [1, 3, 2]
    # [5, 4, 7]
    # [6]
  9. Q69.How should a Python service log, and why use the logging module instead of `print`?

    Use the standard logging module with named loggers, levels and a structured format, configured once at startup. print writes unstructured text with no level, no timestamp and no way to route or filter it.

    • Named loggers: logging.getLogger(__name__) per module lets you tune noisy parts independently.
    • Levels: DEBUG for diagnosis, INFO for business events, WARNING for recoverable problems, ERROR for failures; production usually runs at INFO.
    • Structured output: emit one JSON object per line with fields such as request ID, user ID and duration, so log tools can search and aggregate.
    • Lazy formatting: pass arguments, as in logger.info("order %s", order_id), so messages that are filtered out cost almost nothing.
    • Exceptions: logger.exception inside an except block records the traceback.

    What not to log: passwords, tokens, full card numbers and unnecessary personal data. Add a redaction filter as a safety net.

    Configure handlers only in the application entry point; libraries should create loggers but never add handlers themselves.

    Example

    A small JSON formatter writes one structured line per event, including extra fields and the error summary.

    import json
    import logging
    import sys
    
    class JsonFormatter(logging.Formatter):
        def format(self, record):
            entry = {"level": record.levelname, "logger": record.name, "message": record.getMessage()}
            entry.update(getattr(record, "fields", {}))
            if record.exc_info:
                entry["error"] = self.formatException(record.exc_info).splitlines()[-1]
            return json.dumps(entry)
    
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(JsonFormatter())
    logger = logging.getLogger("orders")
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)
    logger.propagate = False
    
    logger.info("order created", extra={"fields": {"order_id": 42, "total": 499}})
    logger.debug("not shown at INFO level")
    try:
        1 / 0
    except ZeroDivisionError:
        logger.exception("pricing failed", extra={"fields": {"order_id": 42}})
    # Output:
    # {"level": "INFO", "logger": "orders", "message": "order created", "order_id": 42, "total": 499}
    # {"level": "ERROR", "logger": "orders", "message": "pricing failed", "order_id": 42, "error": "ZeroDivisionError: division by zero"}
  10. Q70.How do you add metrics and tracing to a Python service?

    Instrument the service at its edges, incoming requests and outgoing calls, using middleware and client wrappers, so every endpoint is measured without touching business code. Then add a few business metrics by hand.

    Metrics:

    • Record request count, error count and a latency histogram per route and status code.
    • Label by the route template, such as /orders/{id}, never by the raw URL, user ID or order ID; high-cardinality labels create millions of time series and break the metrics store.
    • Add saturation signals: connection pool usage, queue depth, event loop lag and worker utilization.
    • Add business counters, such as orders created or payments failed.

    Tracing:

    • Create a span per request and per outgoing call or query, with timing and status.
    • Propagate the trace context in headers to downstream services, and through async tasks with context variables.
    • Sample normal traffic, but keep traces for errors and slow requests.

    Tie them together: put the trace ID in every log line, so a slow trace leads straight to its logs. Use a vendor-neutral instrumentation standard, so changing monitoring tools does not mean re-instrumenting code.

  11. Q71.How do you write tests with Python's standard unittest framework?

    Group related tests in a class that subclasses unittest.TestCase, write each scenario as a method starting with test, and use the assertion methods so failures report clear differences.

    • Arrange, act, assert: set up inputs, call the code once, and check the result, keeping each test focused on one behavior.
    • Fixtures: setUp and tearDown run around every test; setUpClass runs once per class for expensive shared setup.
    • Table-driven cases: loop over input and expected pairs inside with self.subTest(...), so each failing case is reported separately.
    • Exceptions: assertRaises as a context manager checks that invalid input is rejected.
    • Discovery: test files and methods following the naming convention are found automatically by the test runner.

    What good unit tests look like:

    1. Fast, with no network, database or clock unless the test is about them.
    2. Independent, so they pass in any order.
    3. Named after the behavior, such as test_negative_total_rejected.

    Many teams use a third-party runner for nicer syntax, but the standard framework works everywhere and needs nothing installed.

    Example

    Three tests cover a fee rule, including a table of boundary values and an error case.

    import io
    import unittest
    
    def shipping_fee(total, priority=False):
        if total < 0:
            raise ValueError("total cannot be negative")
        fee = 0 if total >= 500 else 40
        return fee + (60 if priority else 0)
    
    class ShippingFeeTests(unittest.TestCase):
        def test_fee_by_total(self):
            cases = [(0, 40), (499, 40), (500, 0), (1200, 0)]
            for total, expected in cases:
                with self.subTest(total=total):
                    self.assertEqual(shipping_fee(total), expected)
    
        def test_priority_adds_charge(self):
            self.assertEqual(shipping_fee(100, priority=True), 100)
    
        def test_negative_total_rejected(self):
            with self.assertRaises(ValueError):
                shipping_fee(-1)
    
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(ShippingFeeTests)
    result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
    print(result.testsRun, len(result.failures), len(result.errors))  # 3 0 0
  12. Q72.How does mocking work in Python, and why do you patch where a name is looked up?

    unittest.mock replaces a real object with a fake that records how it was called and returns whatever you configure. mock.patch swaps a name for the duration of a test, then restores it.

    Patch where the name is looked up, not where it was defined: from time import monotonic copies a reference into your module. Patching time.monotonic afterwards changes the time module, but your module still holds the original function, so the test has no effect.

    • Rule: patch your_module.monotonic, the name your code actually calls.
    • Alternative: if your code calls time.monotonic() through the module, patching time.monotonic works.

    Using mocks well:

    • Assert on calls with assert_called_once_with to check interactions such as "one email was sent".
    • Use autospec=True so the mock has the real signature and rejects wrong arguments.
    • Use AsyncMock for coroutine functions.
    • Prefer injecting fakes over patching where you can; heavy patching ties tests to the code's internal structure.

    Example

    Patching the source module does not affect a name already imported into this module; patching this module's name does.

    from time import monotonic
    from unittest import mock
    
    def elapsed_since(start):
        return monotonic() - start
    
    with mock.patch("time.monotonic", return_value=100.0):
        print(elapsed_since(40.0) == 60.0)  # False
    
    with mock.patch(f"{__name__}.monotonic", return_value=100.0):
        print(elapsed_since(40.0))  # 60.0
    
    notifier = mock.Mock()
    notifier("order 7 cancelled")
    notifier.assert_called_once_with("order 7 cancelled")
    print(notifier.call_count)  # 1
  13. Q73.How do you test async code in Python?

    Use unittest.IsolatedAsyncioTestCase, which runs each async def test method on its own event loop, so you can await code directly inside tests. Each test gets a fresh loop, so state cannot leak between tests.

    • Async fixtures: asyncSetUp and asyncTearDown open and close resources such as test clients or connection pools.
    • Fakes: replace network clients with small async fakes, or with unittest.mock.AsyncMock, which returns awaitable results.
    • Timeouts: wrap awaited calls in asyncio.timeout to test timeout handling quickly, and so a bug cannot hang the test run.
    • Concurrency: test behavior under concurrent calls by starting several tasks with asyncio.gather.

    Common pitfalls:

    1. Forgetting await makes the test pass while the code never runs; the runtime warns that a coroutine was never awaited, so treat that warning as an error.
    2. Real sleep calls make tests slow; inject a clock or use tiny delays.
    3. Background tasks left running at the end of a test cause warnings and flaky failures, so cancel and await them in teardown.

    Example

    One test checks the result through a fake client, and another checks that a slow call times out.

    import asyncio
    import io
    import unittest
    
    async def fetch_price(client, sku):
        data = await client.get(f"/prices/{sku}")
        return round(data["amount"] * 1.18, 2)
    
    class FakeClient:
        async def get(self, path):
            await asyncio.sleep(0)
            return {"amount": 100.0}
    
    class SlowClient:
        async def get(self, path):
            await asyncio.sleep(1)
    
    class PriceTests(unittest.IsolatedAsyncioTestCase):
        async def test_price_includes_tax(self):
            self.assertEqual(await fetch_price(FakeClient(), "A1"), 118.0)
    
        async def test_slow_dependency_times_out(self):
            with self.assertRaises(TimeoutError):
                async with asyncio.timeout(0.01):
                    await fetch_price(SlowClient(), "A1")
    
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(PriceTests)
    result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
    print(result.testsRun, len(result.failures), len(result.errors))  # 2 0 0
  14. Q74.How do you test Python code that talks to a database?

    Test repository and query code against the same database engine as production, isolate each test's data, and keep pure business logic in unit tests that need no database at all.

    • Same engine: an in-memory substitute behaves differently in types, locking, constraints and SQL dialect, so bugs slip through. Running the real engine in a disposable container during tests is common.
    • Migrations: create the schema by running the real migrations, which also tests them.
    • Isolation: wrap each test in a transaction rolled back at the end, or clean tables between tests, so tests never depend on each other.
    • Factories: small helper functions that create valid records with sensible defaults keep tests short and readable.
    • Realistic checks: test constraints, unique violations, transactions and concurrency, not just simple reads.

    Keep the suite fast:

    1. Separate slow integration tests so they can run in parallel or at a later pipeline stage.
    2. Reuse one database per test run, with per-test isolation.
    3. Avoid mocking the database driver or the ORM; such tests mostly check the mock, not the query.
  15. Q75.Why is unpickling untrusted data dangerous, and what should you use instead?

    pickle can reconstruct arbitrary Python objects, and that reconstruction can call any importable function. A crafted pickle therefore runs code of the attacker's choice the moment it is loaded, before your code ever inspects the result.

    Where this bites backend services:

    • Session data or cookies stored as pickles.
    • Cached values in a shared cache that someone else can write to.
    • Uploaded files, message queue payloads or data from partner systems.
    • Machine learning model files from untrusted sources, which are often pickles.

    What to use instead:

    1. JSON or another data-only format for anything crossing a trust boundary, validated against a schema after parsing.
    2. Signed data: if you must use pickle between your own trusted components, sign it with an HMAC and verify the signature before loading, and keep the key secret.
    3. Restricted loading: limiting which classes may load reduces the risk but is easy to get wrong, so treat it as a last resort.

    The same warning applies to marshal and shelve, and to eval or exec on any text that users can influence.

  16. Q76.How do you prevent command injection when a Python service runs external programs?

    Pass the program and its arguments as a list to subprocess.run, and never build a command string for a shell. With a list, each argument reaches the program as one value, so characters such as ;, | and $ have no special meaning.

    • Avoid shell=True with any input a user can influence; it hands the whole string to a shell that interprets it.
    • Validate arguments against an allowlist of expected values or a strict pattern, because a program can still misinterpret a value that starts with - as an option.
    • End option parsing with -- before user-supplied file names when the program supports it.
    • Set a timeout, capture output, and check the return code, so a hung or failing program does not hang the request.
    • Prefer libraries over programs: a standard-library module or native library avoids process overhead and the injection risk entirely.
    • Least privilege: run the service as a user that cannot do much damage, and give the child process a minimal environment.

    The same thinking applies to file paths built from input, which need a resolved-path check to prevent directory traversal.

  17. Q77.Which standard-library modules should a Python backend use for security-sensitive work?

    Python ships solid building blocks for most everyday security tasks; the main danger is reaching for the wrong module, such as random for tokens or a fast hash for passwords.

    secretshashlibhmacssl
    Generates unpredictable tokens, keys and reset codesProvides hashes plus slow password hashing such as scrypt and PBKDF2Creates and verifies message authentication codesWraps network connections in TLS
    Never use the random module for theseNever store passwords with a plain fast hashCompare tags with compare_digest to avoid timing leaksUse the default context, which verifies certificates and host names

    Other useful pieces:

    • tempfile creates temporary files with safe permissions and unpredictable names.
    • ipaddress parses and checks addresses properly, instead of comparing strings.
    • string.Template substitutes values without the attribute access that str.format allows.

    Where the standard library is not enough: modern password hashing algorithms with memory-hard settings, full cryptographic protocols and certificate management usually need well-reviewed external libraries. Never implement encryption schemes yourself.

  18. Q78.Why is formatting or evaluating user-supplied text dangerous in Python?

    Several Python features treat text as code or as instructions, so letting users control that text lets them run code or read data they should not see.

    • eval and exec execute arbitrary Python. Never call them on user input; parse literals with ast.literal_eval or a proper format such as JSON instead.
    • str.format with user-controlled templates can walk attributes, as in {user.config.SECRET}, and read secrets from objects you pass in.
    • Server-side template injection: rendering a template whose source comes from users can execute expressions inside the template engine. Users should supply values, never templates.
    • Dynamic imports and attribute access such as getattr(module, name) with user-provided names can reach functions you never meant to expose; map allowed names explicitly.

    Safe patterns:

    1. Let users choose from predefined templates, not write them.
    2. Use string.Template with safe_substitute, which supports simple placeholders only.
    3. Pass templates plain dictionaries of strings, not rich objects with hidden attributes.

    Example

    A user-supplied format string reaches a secret through attribute access, while string.Template only fills simple placeholders.

    from string import Template
    
    class Config:
        SECRET = "s3cr3t-key"
    
    class User:
        def __init__(self, name):
            self.name = name
            self.config = Config()
    
    user = User("Asha")
    attacker_template = "Hello {user.config.SECRET}"
    print(attacker_template.format(user=user))  # Hello s3cr3t-key
    
    safe = Template("Hello $name, your order $order shipped.")
    print(safe.safe_substitute(name=user.name, order="#42"))  # Hello Asha, your order #42 shipped.
    print(Template("Hi $user.config").safe_substitute(user="Asha"))  # Hi Asha.config
  19. Q79.How do you keep third-party dependencies secure in a Python project?

    Treat every dependency as code you run with your service's privileges: add it deliberately, pin it exactly, scan it continuously and update it on a schedule.

    1. Fewer dependencies: each one, and everything it pulls in, is attack surface and maintenance work. Prefer the standard library for small needs.
    2. Check before adding: look at maintenance activity, release history, the number of indirect dependencies and whether the exact name is spelled correctly, since look-alike names are a known attack.
    3. Pin with hashes: install from a lock file containing exact versions and hashes, so a tampered or replaced package fails to install.
    4. Scan for known vulnerabilities in the pipeline and on a schedule, because new advisories appear for versions you already run.
    5. Prefer prebuilt wheels: installing from source can execute the package's build code on your machine or build server.
    6. Use a private mirror or proxy for production builds, so builds do not depend on a public index being available and unaltered.
    7. Update regularly in small steps with tests, instead of jumping several major versions during an emergency.
  20. Q80.How would you speed up a slow Python API endpoint step by step?

    Measure where the time goes, fix the largest cost first, and re-measure after every change. In most Python services the database or network dominates, not the interpreter.

    flowchart TD
      A["Measure latency by stage"] --> B{"Database time dominates?"}
      B -->|yes| C["Fix queries, indexes and query count"]
      B -->|no| D{"Waiting on other services?"}
      D -->|yes| E["Run calls concurrently, cache, add timeouts"]
      D -->|no| F["Profile Python code and fix hot spots"]
      C --> G["Re-measure at the 95th percentile"]
      E --> G
      F --> G
    1. Database: remove per-row queries inside loops, select only needed columns, add indexes for the filters used, and paginate large results.
    2. External calls: call independent services concurrently, reuse connections, and cache stable responses.
    3. Python work: profile, then fix algorithms and data structures, avoid repeated serialization, and move heavy computation to background workers.
    4. Response size: return less data, compress large responses, and stream big exports.

    Interview tip: describe your measurement first and the fix second; interviewers listen for whether you would have found the real bottleneck or guessed.