Python Development

Python is where the model code, the data pipeline and the integration glue live. We write it typed, lock it reproducibly, and keep the CPU-bound work out of the request path — and we say plainly when Go or Node is the better runtime for the service you actually need.

What Python is

Python is a dynamically-typed, garbage-collected language whose value in production is almost entirely its library surface: NumPy, Pandas, Polars, scikit-learn, PyTorch, Arrow, FastAPI, Django. The interpreter itself is not fast, and for a long time a global interpreter lock meant one process executed one line of Python bytecode at a time — but the heavy numeric libraries drop into C, Rust or CUDA and release that lock, so well-shaped Python spends most of its wall-clock outside the interpreter. Modern Python is a typed language in practice: type hints plus mypy and Pydantic give a codebase the checkable contracts that make it survivable past twenty thousand lines.

Prefer the short definition? Read the Python glossary entry.

The honest version

When Python is the right choice — and when it is not

Choosing a technology because it is popular is how projects end up expensive. Here is where we would pick it, and where we would tell you not to.

Good fit

Machine learning and model serving
Every serious ML framework treats Python as its primary API and the other language bindings as second-class and behind. Training loops, feature engineering, evaluation harnesses and inference all in one language means the preprocessing that produced a feature at training time is literally the same function that produces it at serve time — which removes the single most common cause of a model behaving worse in production than in the notebook.
Data pipelines where transformation correctness dominates
The work is expressing joins, windowing, deduplication and late-arriving-data rules correctly, not shaving microseconds. Polars and DuckDB do the actual computation in Rust and C++ over Arrow memory, so Python is only orchestrating; and the ecosystem for testing a transformation against a fixture set is far richer here than anywhere else.
Integration and file-format glue
Parsing an HL7v2 segment, a FHIR bundle, a bank's fixed-width settlement file or a partner's badly-formed CSV is where Python's library depth and its tolerance for messy input pay off. These jobs are I/O-bound and run in workers, so interpreter speed is irrelevant and iteration speed is everything.
API services that sit directly on top of data or model code
When the endpoint's job is to run a model, query a warehouse or apply a scoring rule, splitting the service into a fast API in one language calling Python over the wire adds a network hop, a second deployment and a serialisation contract to maintain. FastAPI with Pydantic v2 — whose validation core is compiled Rust — is quick enough that the hop is not worth buying.
Admin-heavy internal products
Django gives you authentication, permissions, migrations, an audit-capable admin and a mature form layer on day one. For an operations tool used by fifty internal staff, that head start is worth more than any runtime characteristic, and the constraint of Django's conventions keeps a small team from inventing five ways to do the same thing.

We would choose something else

Latency-critical, high-fanout network services
CPython carries meaningful per-request interpreter overhead, and its garbage collector introduces pauses you cannot schedule. At the same core count, a Go service holds a much tighter p99 on the same workload, because goroutines are cheap, the runtime is compiled, and the GC is tuned for short pauses. If your budget is a single-digit-millisecond p99 on a gateway or an auth hop that every other request waits behind, we write that service in Go and keep Python for the work behind it.
CPU-bound parallelism inside one process
Pure-Python CPU work does not scale across cores in a standard interpreter build — the global interpreter lock serialises bytecode execution, so eight threads give you roughly one core of throughput. The workarounds are real but costly: multiprocessing pays pickling and copied-memory overhead on every task, and dropping into C or Rust means maintaining a second toolchain. Free-threaded builds have moved from experimental to officially supported, but the binary wheel ecosystem is still catching up and single-threaded performance is slightly worse, so we do not yet stake a delivery date on them. If the workload is genuinely parallel, genuinely CPU-bound and genuinely written in Python, the language is the wrong tool and no amount of tuning fixes it.
Cold-start-sensitive serverless and edge functions
A Lambda or Cloud Function that imports NumPy, Pandas or Torch is a very large deployment artefact whose import time alone runs into seconds before your handler executes — Python's import machinery walks the filesystem and executes module-level code for every dependency. A Node or Go function on the same path starts in tens of milliseconds. For bursty, latency-visible serverless endpoints we use Node or Go, or we put the Python work behind a queue with warm workers instead of on the synchronous path.
Memory-tight or high-density workloads
Python objects carry substantial per-object overhead, and a long-running worker holding a large in-memory index, cache or graph will occupy several times the resident memory of an equivalent Go or Rust process. On ECS or Kubernetes you pay for reserved memory, so this shows up directly as a bill and as fewer replicas per node. When the design calls for many small, dense, long-lived processes, Python is the expensive choice.
Anything that must run in a browser or as native mobile code
WebAssembly runtimes for Python and cross-compilation projects for mobile exist and are genuinely interesting, but neither has the deployment story, bundle size or debugging tooling you want on a product a client is shipping to customers. Frontend and mobile work goes to TypeScript and React Native, and the Python stays on the server.

In practice

What we build with Python

  • Model inference services — FastAPI in front of PyTorch or ONNX Runtime, with request batching, explicit model versioning, shadow-traffic comparison between versions, and preprocessing that is the same code the training job used rather than a reimplementation that quietly drifts.
  • Data and ETL pipelines — Dagster or Airflow orchestration over Polars, DuckDB and Arrow, with idempotent tasks, partition-keyed backfills that run the same code path as live processing, and warehouse loads that are re-runnable after a bad day without duplicating rows.
  • Record and document integration workers — FHIR and HL7v2 payloads, bank statement and NACH/UPI reconciliation files, e-invoice and fixed-width formats — parsed in queue-backed workers with retry policies, dead-letter handling and a stored raw copy of every input for dispute replay.
  • Back-office and internal operations systems on Django — role-based permissions, audit trails, maker-checker approval flows and operational reporting, where the admin framework, migrations and auth that ship with Django remove weeks of undifferentiated work.
  • Scheduled automation and reconciliation jobs — the settlement match, the nightly export to a regulator's format, the drift check on a deployed model — instrumented so a failed run is an alert with a trace, not a silent gap someone notices a week later.

Positions

Decisions we have already made

Defaults we start from. They are arguable — but they are argued, not assumed, and we will change them for a good reason.

FastAPI for services, Django for admin-heavy products — and we pick deliberately, not by habit
FastAPI with Pydantic v2 is our default for anything that is mostly an API surface over data or a model: the validation core is compiled Rust rather than interpreted Python, the ASGI stack handles I/O fanout properly with asyncio, and the OpenAPI schema falls out of the type annotations so the TypeScript client is generated rather than hand-written and stale. We switch to Django when the product's centre of gravity is CRUD, permissions, back-office screens and audit history — there, reimplementing the admin, the auth model and the migration tooling to get async you do not need is a bad trade.
SQLAlchemy 2.0 with typed models and Alembic, and hand-written SQL for reporting
We use the 2.0 style — explicit select() constructs, typed mapped columns, and loader options stated per query so N+1 patterns are visible in code review rather than discovered in a slow-query log. Alembic migrations are reviewed like application code, with the downgrade path actually considered. Analytical and reporting queries are written as SQL and kept in version control, because expressing a window function and three CTEs through an ORM produces something nobody can read and the planner cannot be reasoned about. The ORM's job is transactional access, not report generation.
mypy in strict mode, ruff for lint and format, uv for resolution and locking — enforced in CI
Untyped Python is fine at five thousand lines and a liability at fifty thousand. New modules are written under strict mypy and the build fails on type errors, not warnings; legacy modules are migrated file by file rather than with a blanket ignore. Dependencies are resolved and locked with uv, with hashes pinned, so the wheel installed in production is byte-identical to the one tested in CI. Formatting is not a discussion — ruff decides.
Containers on ECS Fargate or Cloud Run, not Lambda, for anything with heavy dependencies
Python services ship as multi-stage container images: dependencies resolved from the lockfile into a wheel layer, application code copied on top, non-root user, pinned base image. Gunicorn manages Uvicorn workers, sized to the cores actually allocated rather than to the host's core count, which is the usual cause of a container thrashing under its CPU limit. GPU inference runs on its own node group with the CUDA and framework versions pinned into the image, never in the same pod as the API — most 'works locally, fails in production' ML incidents are a driver and CUDA version mismatch, and pinning the whole stack into one artefact removes the category.

Architecture

Things worth getting right early

Keep the interpreter lock off the request path
Concurrency in a Python service comes from I/O, not from threads executing Python. Async handlers await database and HTTP calls; numeric work goes through NumPy, Polars or an inference runtime that releases the lock while it computes in native code; anything genuinely CPU-bound is handed to a worker pool over a queue and answered asynchronously. The failure mode we design against is specific: a single synchronous CPU-bound call inside an async handler blocks the event loop for every other in-flight request on that worker, so a function that takes 200ms of pure Python turns into a latency cliff for unrelated users. That rule is a code review item, not an aspiration.
A typed, explicit boundary at every edge
Python's flexibility is useful inside a module and dangerous across a service boundary. Request and response bodies, queue message payloads, and model input and output are all Pydantic models with versioned schemas, so an unexpected field or a changed type fails at the edge with a clear error rather than surfacing three layers deep as an attribute error at 2am. The same discipline is what makes a later rewrite cheap: if the contract is explicit and narrow, moving one hot service to Go is a contained piece of work rather than an archaeology project.
Every job idempotent, keyed and replayable
Background work assumes at-least-once delivery, because that is what every broker actually gives you. Tasks carry an idempotency key, writes are upserts or guarded by a uniqueness constraint, failures land in a dead-letter queue with the original payload intact, and a backfill runs the identical code path as live processing with a different partition range. This is what makes a bad deploy or an upstream outage recoverable by re-running a date range instead of by writing a one-off repair script against production.
Reproducible builds, or the ML stack will betray you
Dependency resolution is locked with hashes, base images are pinned by digest rather than by tag, and installs are wheel-only where possible so no build toolchain has to exist in the runtime image. For anything touching GPUs, the framework, CUDA and driver-compatible runtime versions are fixed together in a single image and upgraded as one deliberate change. Telemetry is OpenTelemetry from the start, with trace context propagated through the queue so a slow inference request can be followed from the API span into the worker that served it.

Around it

What Python usually sits next to

  • PostgreSQL

    The default datastore behind Python services — SQLAlchemy 2.0 with Alembic migrations, JSONB for raw inbound payloads before they are normalised into typed columns, and partitioning for large event and audit tables.

  • Redis

    Broker for Celery or RQ workers, feature cache in low-latency scoring paths, idempotency-key store for at-least-once task handling, and rate limiter in front of expensive inference endpoints.

  • Docker

    Multi-stage images are how Python's dependency problem stops being a deployment problem — the lockfile resolves once at build time and the CUDA and framework versions ship pinned inside the artefact.

  • TensorFlow

    One of the two training frameworks we work in, with export to a portable runtime format so the serving path does not need to carry the full training stack into production.

  • TypeScript

    The other half of most products we build in Python. The FastAPI OpenAPI schema generates the TypeScript client, so a change to a response model breaks the frontend build rather than a user's screen.

  • OpenTelemetry

    Instrumentation for FastAPI, SQLAlchemy and the worker queue, with trace context carried across the async boundary so an inference request is traceable from the HTTP span to the model call.

Questions

Common questions about Python

  • Does the global interpreter lock actually matter for my application?

    Usually less than people expect, and occasionally a great deal. If your service is I/O-bound — waiting on a database, an HTTP call, a queue — the lock is released during the wait and asyncio gives you real concurrency. If your heavy computation runs inside NumPy, Polars, an inference runtime or a database, that work happens in native code with the lock released. The lock bites when you have significant pure-Python computation you want to run in parallel threads inside one process. Free-threaded interpreter builds now exist as a supported option and remove the restriction, but not every binary wheel your project depends on is ready for them yet, so we treat that as a per-project assessment rather than a default. If we find the lock is your real constraint, we will tell you and move that component to Go rather than tune around it.

  • Python or Node for our backend?

    It depends on what the backend spends its time doing. If it is mostly orchestrating I/O for a web or mobile product — auth, CRUD, third-party calls, websockets — Node with TypeScript is a fine choice and lets one team share types across frontend and backend. If the backend's real work is a model, a data transformation or a nontrivial computation, Python wins because that work already lives there and duplicating it across a language boundary is where bugs breed. Plenty of the systems we build use both: Node or Go at the edge, Python for the data and model services behind it, with a schema-defined contract between them.

  • Is Python fast enough to serve a production API?

    For most business APIs, yes — the database round trip and the network usually dominate, and Pydantic v2's compiled validation core removed what used to be the biggest interpreter cost in a FastAPI request. Where it is not fast enough is the tail: if you need a predictable single-digit-millisecond p99 under high concurrency, or you are running a component every other request queues behind, a compiled runtime holds that budget with far less effort. We would rather scope that constraint honestly at design time than have you discover it during a load test.

  • Django or FastAPI for our project?

    Django if the product is fundamentally screens, records, roles and approvals — you inherit the admin, the auth model, the migration tooling and a huge amount of well-trodden convention, and for an internal operations system that is most of the build. FastAPI if the product is an API consumed by a separate frontend or by other services, particularly one serving models or data, where typed schemas, generated OpenAPI and async I/O matter more than batteries-included screens. Both are good; picking the one that matches the shape of the work saves more time than any framework benchmark.

  • If we prototype in Python, will we have to rewrite it later?

    Usually not, and where you do, only a small part of it. The pieces that occasionally need to move are hot request-path services with tight latency budgets, not the pipelines, model code or integration workers. We design for that outcome from the start by keeping service boundaries explicit and typed, so replacing one component means reimplementing a documented contract rather than reverse-engineering a codebase. What is not sustainable is untyped Python that grew for two years without discipline — which is why we run strict typing and locked dependencies from the first commit rather than as a later cleanup.

Services that use Python

Where it lands

Tell us what the Python has to do

Send the shape of the workload — the model you want served, the pipeline that keeps breaking, the reconciliation job someone runs by hand, or the Django codebase that has outgrown its first author. We will come back with an architecture position, an honest read on whether Python is the right runtime for the hot path, and what the work looks like in stages. If Go or Node is the better answer for part of it, that will be in the reply too. Work starts from around Rs 25,000 for a single focused piece and scales to long-running platform engagements.