A typed boundary, a safe migration, and a deploy on merge.
Python has a reputation on the server: the place where data science leaks into production. It is a fair reputation, and it is earned by engineering rather than by the language. A Python service can be exactly as well built as a Node one — validated at the edge, tested around what carries money and data, migrated deliberately, observable, and shipped when the branch merges.
That is the standard we hold it to, and it is the same standard on every page of this site. The three failures below are the ones we are called in for most, and none of them is about the language.
PanoptesAI fuses acoustic, RF, electro-optical/infrared and radar into one validated track — detection to handoff in four seconds. Its case page names the stack, and FastAPI is in it.
A membership platform we built runs on Python and Django with a React front end over PostgreSQL and MongoDB — the Django half of this page, named on its own case page rather than asserted here.
We work in Flask a great deal, and no case page on this site names it — most of that work is internal tooling and integration services that never become published case studies. So the third column below tells you what we do with it rather than pointing at evidence we do not have.
30 minutes, working session. Bring the endpoint you are least happy with and the schema change you have been avoiding; you leave with a one-page read either way. Nobody follows up more than once.
Six ways a Python service goes wrong, and what is actually causing each
Read the left column and you will recognise at least two of them. The right column is the part that gets skipped in the meeting where somebody proposes rewriting the service in another language.
Fast on the laptop, unusable on the real table
An ORM relationship that reads like one line and issues four hundred statements, or a filter that has no index behind it. Staging has ten thousand rows and production has ten million, so the plan flips from an index scan to a sequential one and nobody was watching. Nobody has read the query plan, which is a five-minute job that ends the argument.
The schema change that queued every write for twenty minutes
A naive ALTER on a large table, deployed at ten in the morning, taking an exclusive lock while it rewrites. It is now folklore, and every schema change since has been deferred — which is how a team ends up with a data model it has outgrown and is afraid to touch. The instrument below is this exact event.
The async service that is secretly synchronous
One blocking library call inside an async handler — a client that was never awaitable, a file read, a CPU-bound loop. Under light load nothing shows. Under real concurrency the whole worker stalls behind it, the mean latency barely moves and the tail turns into a saw. Blocking work belongs in a thread pool, and the pool has to be sized against worker concurrency rather than guessed.
The build that worked last month
No lock file, a requirements list of unpinned names, and a transitive dependency that shipped a release on Tuesday. The image on the laptop and the image in the pipeline are two different pieces of software that happen to share a name. Pinned and hashed dependencies turn this from a weekly surprise into a decision somebody makes on purpose.
The background job that stopped on Tuesday
A loop in a process nobody supervises: no retries, no backoff, no dead-letter path, no alert. Somebody noticed on Friday because a customer asked. Background work is a system with its own failure modes, and it needs the same treatment as the request path — idempotency so a replay does not duplicate records, and an alert into a channel a person actually reads.
Nobody can say which model produced last Tuesday’s output
The model is loaded at import, held in memory per worker and versioned with the application, so changing it is a deploy and reverting it is a redeploy. Once the model has its own version, its own storage and its own rollback, the data team stops waiting on the release train and the service stops carrying a payload it cannot describe.
Two ways to add a column, drawn on the same clock
A table taking steady writes, and one new required column. Above, the change everybody writes first. Below, the same change by expand and contract. Scrub through two minutes and watch the queue: the second one is not cleverer, it is simply four small steps instead of one long one.
Nothing has happened yet. The table is taking its usual writes and every one of them is served.
The same table, the same load, the same new column. Four steps instead of one, each of them short enough that nothing queues behind it.
The point is not that one is difficult. Adding a nullable column, backfilling it in batches, adding the constraint once the data is there and dropping the old path afterwards is ordinary work with a checklist. The point is that a migration is the one change you cannot undo by redeploying, so it is the one that gets reviewed hardest — and once a team has a procedure for it, schema changes stop being deferred.
Insight without the individual, at a size where the naive step never finishes
Anonymised mobile-location data turned into footfall and behaviour insight for a location-intelligence company. Python is named in the stack on its case page. The hard part is that the aggregation has to hold a privacy constraint at a data volume where a single-pass pandas step simply does not return — so the shape of the pipeline is decided by memory and by batching long before anyone argues about the language. Location intelligence case →
Three names in the title, and an honest weight behind each
The useful version of this section is the one that tells you where the evidence is. All three are in daily use here. Two of them have a published case page naming them and one does not, and that asymmetry is written below rather than smoothed over — because on a page like this, a claim you can check is worth more than a claim you cannot. What does not vary is the engineering standard: the framework decides the idioms, not the bar.
Where new services go by default: Pydantic models as the boundary contract, request validation that produces an error a client can act on, and an OpenAPI document generated from the same types the code runs on, so it stays truthful instead of drifting into fiction. It is in the published stack for PanoptesAI, alongside Python, OpenCV, deep learning and sensor fusion.
The async model is the reason to choose it and the reason to be careful with it — see the third symptom above.
Where a product already runs on it, and where the admin, the auth, the ORM and the migration framework are worth more than the freedom to assemble your own. We built a membership platform on Python and Django with a React and TypeScript front end over PostgreSQL and MongoDB, integrated with Webflow.
A large Django application is the normal end state of a successful one. It gets split module by module, with tests pinning the behaviour that has to stay identical.
We work in Flask a great deal. Most of it is the software that never becomes a case study: internal tooling, integration services, webhook receivers, admin surfaces and the layer wrapped around somebody’s model — load-bearing for the client, and invisible from the outside.
It earns its place where the surface is small and the team wants to see the whole application at once, and where an existing Flask codebase is doing its job and needs maintaining rather than replacing. On a new service that will grow a large typed API surface, we would say what FastAPI’s generated schema buys you by month three before you commit.
The answer to “which framework will you use” is: whichever your team can still maintain after we leave. A framework nobody in-house can read is a liability wearing the costume of an upgrade.
Four systems, and what was hard about each
In every one of these, Python is the service around something that has to be right and fast. Each has a case page behind it, except the last, which is named honestly for what it is.
Python · SQL
Privacy-safe
Aggregation that has to hold a constraint at volume
Anonymised mobile-location data turned into footfall and behaviour insight for a location-intelligence company. The hard part is volume with a privacy constraint attached: the output has to be genuinely useful about a place while being unable to say anything about a person, at a data size where the obvious single-pass implementation does not finish. That is a memory and batching problem long before it is a framework one. Location intelligence case →
FastAPI · sensor fusion
Four seconds
Reconciling instruments that each disagree differently
Acoustic, RF, electro-optical/infrared and radar fused into one validated track, detection to handoff in four seconds. The hard part is that four sensors produce four different kinds of confidence, and an operator needs one object with one status rather than four opinions. The service in front of that is a FastAPI service, and the console in this page’s first screen is what it serves. PanoptesAI case →
Perception service
Shared budget
A Python service inside somebody else’s latency budget
Real-time distance and head-angle tracking running beside speech recognition, speech synthesis and a 3-D character in a browser. We prototyped on OpenCV, then benchmarked two pipelines rather than picking by reputation, and shipped the fastest available. The hard part is that this service does not own its budget — it shares a few hundred milliseconds with everything else in the experience, so “fast enough” is defined by neighbours it does not control. Face-tracking case →
n8n · LLM classification
300+ / week
The one where the right answer was not a Python service
CRM, email, project management and reporting joined into one automated flow, with a model on the judgement steps only: 300+ records a week automated, roughly 80% less manual entry in month one. It is on this page because it is built from the same unglamorous half that a good background worker is — retries with backoff, idempotency so a replay does not duplicate a record, dead-letter handling and an alert somebody reads. And because it is honest to say that we reached for a workflow tool rather than writing a service, which is sometimes the correct call. Process automation →
Bring one slow endpoint and one deferred migration to the Data Flow Audit. Those two answer most of what we would otherwise spend a week discovering.

What a Python service of ours has on the day it ships
These are not options on a quote. They are what the work includes, and the reason the second year of the service costs what the first one did.
Pydantic models as the contract, validation that produces an error a client can act on, an OpenAPI document generated from the same types the code runs on, pagination that survives inserts, idempotency keys where a retry could duplicate something, and versioning that lets an old client keep working after today’s deploy.
Migrations reviewed as carefully as code, because they are the only change a redeploy cannot undo. Expand and contract as the default shape, backfills in batches, and a written procedure your team can follow the week after we finish.
Gunicorn or Uvicorn worker models chosen rather than copied, memory per worker measured so the instance size is a decision, connection pools sized against worker concurrency, timeouts on every outbound call, and blocking work moved off the loop into a pool that is sized for it.
Pinned and hashed dependencies with a lock file in the repository, a virtual environment in CI, wheels rather than surprise source builds in the image, and a container that is the same piece of software on a laptop and in the pipeline.
Coverage concentrated where money and data move rather than spread evenly for a number. Behaviour pinned before anything is refactored, and a suite stable enough that a red build still means something in month six.
Structured logs with a correlation id that survives a queue hop, metrics with cardinality you can afford, and alerts tuned so people still read them in month three. A senior architect signs off the design, and you talk to the engineers doing the work rather than to an account layer.
The photograph above is from the membership-platform build — the Django half of this page, and the end of a working day with the client’s team. Relationship-support platform case →
Opinions with a date on them
Nearly everything on this page is a position that was correct at some point and will be revised. Async clients that were unusable three years ago are fine now; a packaging tool that was a curiosity is the default; the advice about the GIL changed underneath everyone. We keep the opinions current by being where they are argued out, and by rebuilding our own things often enough to notice when one goes stale. About Fluvius →
The service in front of a model, and the line we draw before starting
This is where most of our Python lives, and it is also where the most territory gets defended. So the boundary gets written down in week one: your data or ML people own the model and the method; we own everything between it and the product.
The model has its own version, its own storage and its own rollback, so changing it is not a deploy and reverting it is not a redeploy. After that, every output can be traced to the version that produced it — which is the question somebody asks eventually, usually under pressure.
Load time, memory per worker and the first-request penalty all measured rather than assumed, because together they decide the instance size and therefore the bill. Batching where the traffic shape rewards it, and left alone where it only adds latency.
A typed request and response between the product and the model, so the data team can change what is inside without breaking a client, and the product team can read the schema instead of the notebook.
We do not rewrite anyone’s model on principle, and we do not port it to another language to feel better about it. Their work stays theirs; we build the boundary, the versioning, the deployment and the monitoring around it, and we write that division down before anybody starts.
If the deeper question is about the model itself rather than the service around it, that is the neighbouring page: PyTorch, TensorFlow & Pandas covers training, evaluation and what a model costs to own.
The work, as we actually sell it
Timelines are indicative ranges and depend on scope. Every one of these starts small enough to cancel, and each produces something you keep.
A Python service built properly
6–14 WEEKSTyped boundary, generated and truthful OpenAPI, tests around what carries money and data, migrations reviewed like code, and a deploy that happens when the branch merges. Delivered as a working slice inside your product rather than as a standalone repository.
The slow endpoint, with numbers
2–6 WEEKSQuery plans read on your data, N+1 removed, select_related and prefetch_related used deliberately, and indexes added where they earn their write cost. You get the before-and-after measurements rather than an assurance.
Zero-downtime schema change
2–5 WEEKSExpand and contract as a repeatable pattern, backfills that run in batches without holding a lock, and a written procedure your team follows on the next change without us in the room.
Async, done deliberately
2–6 WEEKSFinding the blocking calls hiding inside async handlers, moving them where they belong, sizing pools against worker concurrency so the database is not exhausted, and setting the outbound timeouts nobody set.
The service in front of a model
4–10 WEEKSModel versioned separately from code, warm-up and memory per worker understood, batching where it helps, and a rollback that does not require redeploying the application to reach.
Background work that survives
2–6 WEEKSRetries with backoff, idempotency so a replay is safe, dead-letter handling with somewhere for a failed job to go, and alerting into a channel your team actually reads. RabbitMQ where the routing and the delivery guarantees matter, Celery or RQ for Python workers, and BullMQ over Redis where the workers are Node — which is common on a mixed team and no reason to run two brokers.
Staged modernisation of a large Django app
12–26 WEEKSModule by module, with tests pinning the behaviour that has to stay identical, and the system in service the whole way through. Length depends on surface area, and the first module is deliberately small.
A reproducible build
1–3 WEEKSPinned and hashed dependencies, a lock file in the repository, a clean environment in CI and an image that stops depending on a system library nobody declared. The shortest engagement here and often the one that stops the most weekly noise.
What you are probably thinking
The record behind the pages
Many of our clients have been with us for 7+ years straight. The Upwork and Clutch records are independently verifiable.
Looking for this as an engagement rather than a stack? Full-Stack Web Development sells the team and the outcome, and Legacy Modernization covers the staged split of a large application. This page is the tool-level view underneath both. See also all our work.
Not ready to talk? Take the checklist.
One page: 14 questions about your Python service you should be able to answer in under a minute. Whether your dependencies are pinned and hashed. Which async handlers contain a blocking call. Whether the pool size matches worker concurrency. Which migrations take a lock and how long they hold it. Where the model version lives. What happens to a background job that fails at three in the morning. And which endpoints have no timeout on their outbound calls.
Bring the endpoint you like least, and the migration you keep postponing.
A free 30-minute working session, not a sales call and not a demo — there is nothing to demonstrate. Show us the slow path and the schema change you have been avoiding, and we tell you what we would do first and in what order.
You keep a one-page read either way: the likely cause of the slow path with the measurement we would take to confirm it, the safe shape of the migration you have been deferring, and the one test we would write before touching anything. Nobody follows up more than once.