Languages
Python Development Company
The language we reach for when the work is data, models or automation, and the one we will talk you out of when it is not.
Overview
Python is a general-purpose, dynamically typed language whose defining trait is readability. Code that looks close to executable pseudocode, with a strong cultural preference for one obvious way to do most things. That legibility is why it became the default language for people who write software occasionally rather than for a living, researchers, analysts, quants, operations engineers, and it is why so much of the world’s data and machine-learning work now happens here. The language itself is fairly ordinary. Nothing in Python’s syntax is a breakthrough. The ecosystem that grew on top of it is the actual product.
That ecosystem is the whole argument. NumPy gave Python vectorised arrays backed by optimised C and Fortran, which turned a scripting language into a credible numerical tool. pandas made tabular data manipulation routine. scikit-learn made classical machine learning a matter of a few lines rather than a research project. PyTorch and TensorFlow made Python the language deep learning is published in, so new techniques land here first and arrive elsewhere, if at all, months later. The modern LLM tooling, the vendor SDKs, the vector database clients, the retrieval and agent frameworks, are Python-first by a wide margin. On the web, Django gives you a full framework with an ORM and a generated admin, while FastAPI has become the standard way to put a typed, async, well-documented HTTP surface in front of Python code. For everything in between, scripting, glue, one-off extracts, scheduled jobs, Python is the tool most teams already have installed on every machine that matters.
We build and operate Python in production rather than in notebooks: ingestion and transformation pipelines, model training and serving, retrieval and LLM systems, internal automation, and the FastAPI or Django services that put an interface on all of it. The engineering discipline we bring is mostly about boundaries. Which parts of a system genuinely need to be Python, which parts must not be, and where the seam between them lives. Get those boundaries right and Python is a joy to maintain for years. Get them wrong and you end up with a slow, untyped monolith that nobody wants to touch.
We are equally direct about the limits, because most of the value in hiring senior people is knowing where the walls are. Pure-Python CPU-bound code is slow in a way no amount of cleverness fixes. The Global Interpreter Lock means threads will not give you multi-core parallelism for compute. Packaging and dependency management remain a real source of friction, even with better tooling than we had five years ago. And Python has no serious place on mobile or in the browser. Knowing all of that in advance is not pessimism, it is what stops a project discovering it at load-test time.
Best for: Data-heavy back ends, machine learning and AI systems, and the automation that ties an estate together, anywhere the ecosystem does the heavy lifting and raw single-threaded speed is not the binding constraint.
Why teams choose Python
The ecosystem does the work you would otherwise fund
From NumPy and pandas through scikit-learn and PyTorch to the entire modern LLM and retrieval stack, the libraries you need already exist, are widely used and have had their sharp edges filed off by thousands of teams. Your budget goes on the part of the problem that is actually yours, not on rebuilding primitives somebody else has already got right.
Readable code survives its authors
Python’s syntax makes intent obvious, which matters far more than cleverness on a system that will live for years and be maintained by people who were not in the room when it was designed. New engineers become useful quickly, and code review argues about the logic rather than decoding the language.
One language across research and production
The model a data scientist trains and the service that serves it can share types, code and tooling. That removes an entire integration seam, the one where a model gets rewritten in another language and quietly behaves differently, which is a common and expensive class of production bug.
Optional typing when you want it
Type hints checked by mypy or pyright turn a dynamic language into one you can refactor across confidently, without giving up the exploratory speed that makes Python worth using in the first place. You choose the level of rigour per module rather than for the whole project at once.
Escape hatches that actually work
When a hot path genuinely needs compiled speed, Python gets out of the way. Cython, native extensions, ctypes, cffi and PyO3 let you replace the small fraction of code that matters without rewriting the system around it. That graduated path is why Python survives in workloads it has no business being fast at.
Why businesses choose Python
- Your system is built around data, models or AI, and you want to use the ecosystem rather than fund a partial reimplementation of it in another language.
- Your requirements are still moving, and readable code you can change next month is worth more than micro-optimised code you cannot.
- Your real bottleneck is I/O, waiting on databases, queues and third-party APIs, where async Python is entirely fast enough and the constraint sits elsewhere.
- You need one language to span exploratory analysis, model training and the production service, so the thing you validated is the thing you run.
- You want engineers who will profile before optimising, push hot paths into compiled code when the measurement justifies it, and tell you plainly when a component should be written in something else.
What we build with Python
The capabilities this technology is genuinely strong at, and what we most often build with it.
The numerical core
NumPy provides vectorised n-dimensional arrays whose operations execute in optimised C and Fortran, so array-level work runs at compiled speed under a clean Python surface. SciPy adds the numerical methods, optimisation, interpolation, signal processing and linear algebra, that would otherwise be a research effort on their own. Almost every performance conversation in Python data work comes down to whether you are operating on arrays or looping over elements.
pandas and the dataframe workflow
pandas made tabular manipulation, joins, grouping, reshaping, time-series resampling, a routine part of every data engineer’s day. It is superb for exploration and for pipelines of moderate size. We are also candid that it is memory-hungry and single-threaded by default, so for larger workloads we reach for Polars, DuckDB or a distributed engine rather than pretending a bigger machine solves the problem indefinitely.
Machine learning and deep learning
scikit-learn for classical models, gradient boosting libraries for tabular problems where they usually win, and PyTorch or TensorFlow for neural networks. The research community publishes in Python, so techniques become usable here first. We treat model selection as an engineering decision, not a fashion one, and the boring model that is easy to explain and cheap to serve frequently beats the exciting one.
FastAPI and typed async services
FastAPI uses Python’s type hints to validate and coerce requests, generate OpenAPI documentation automatically, and serialise responses, on top of an async runtime that handles high concurrency for I/O-bound work. It has become our default for API-shaped Python services because the types you write for the compiler are the same ones that document and enforce your contract.
Django for full applications
When the application needs an ORM, migrations, authentication, permissions and a generated admin rather than just an HTTP surface, Django is the mature answer. We pick between the two on the shape of the application, not on preference, and we are perfectly happy running a Django application that talks to a separate FastAPI inference service.
Pipelines and orchestration
Airflow, Prefect, Dagster and, where the job is simple enough, plain scheduled containers. The orchestration layer is where data engineering succeeds or fails, because it owns retries, idempotency, backfills, dependencies and the question of what happens when yesterday’s run silently produced nothing. We design pipelines to be re-runnable without fear.
LLM and retrieval tooling
The vendor SDKs, LangChain and its alternatives, embedding models, vector stores and evaluation harnesses all live in Python first. We build retrieval, agent and tool-using systems here for exactly that reason, with real evaluation rather than vibes, because an LLM system without measurement is a demo that will embarrass you later.
Static analysis and quality tooling
Type hints validated by mypy or pyright, ruff for extremely fast linting and formatting, pytest for tests, and lockfiles for reproducible environments. None of this is optional on a codebase intended to last. We wire it into CI from the first week so the discipline is structural rather than a matter of who remembers.
Native interop
Cython, C and C++ extensions, ctypes, cffi and Rust bindings through PyO3 let the small performance-critical portion of a system run compiled while the rest stays readable Python. Many of the libraries you already depend on work exactly this way, which is why Python can be fast in practice while being slow in principle.
Use cases
Data ingestion and transformation pipelines
Pulling data from APIs, databases, files and event streams, validating and reshaping it, and landing it somewhere analysts and downstream systems can trust. The engineering value here is in idempotency, schema handling, error visibility and backfills, not in the transformation logic itself, which is usually the easy part.
Model training and serving
Training in PyTorch or scikit-learn and serving behind a FastAPI endpoint, with the same language spanning experiment and production. We version models and their inputs, keep the feature computation identical between training and serving, and treat drift monitoring as part of the build rather than something to add after the first bad quarter.
LLM applications and retrieval systems
Document ingestion and chunking, embeddings, vector search, prompt and tool orchestration, and the evaluation harness that tells you whether a change made things better or just different. Python is where this ecosystem lives, and building it anywhere else currently means fighting the tooling.
Internal automation and operational glue
Replacing brittle shell scripts and manual processes with tested Python for deployments, reconciliations, reporting, data migrations and the hundred small jobs that keep an operation running. This is unglamorous work that quietly removes a great deal of risk.
Analytics and reporting back ends
Services that aggregate, compute and expose figures the business actually makes decisions on, where correctness matters more than latency and where being in the same language as the analytical work removes a translation step that otherwise introduces subtle disagreements between two versions of the truth.
Scientific and engineering computation
Simulation, optimisation and modelling work where SciPy and the wider numerical stack provide methods that would take months to implement correctly, and where the honest engineering job is wrapping research code in something reproducible, tested and deployable.
When Python is the right choice
- Right when the work is data, statistics, machine learning or AI. The scientific and ML stack has no equal in any other language, and choosing anything else means either reimplementing it or building an awkward bridge back to it. This is Python’s home ground and it is not close.
- Right for automation, orchestration and glue. Stitching services together, moving and reshaping data, reconciling systems that were never designed to talk, replacing a directory of fragile shell scripts with something readable, tested and version-controlled. Python is unusually good at being the connective tissue of an estate.
- Right for APIs and back ends where throughput is I/O-bound rather than compute-bound. A single async FastAPI process holds a large number of concurrent connections comfortably, because almost all of the time is spent waiting on a database, a queue or an upstream service, and waiting is something Python does as fast as anything else.
- Right when the shape of the problem is still uncertain. Dynamic typing and a huge standard library get you from idea to running prototype in hours, and for data work, where you often cannot specify the solution until you have looked at the data, that iteration speed genuinely changes outcomes.
- Wrong for CPU-bound hot loops written in pure Python. Tight numerical algorithms, real-time signal processing, low-latency trading paths and anything where microseconds are the unit of account belong in C, C++, Rust or Go, or must be pushed down into compiled extensions that release the interpreter lock. We will say this early rather than after a disappointing benchmark.
- Wrong for mobile applications and browser front ends. There is no credible native mobile story and nothing that makes running Python in a browser a sensible production choice today. Use Swift or Kotlin, or Flutter or React Native, on the client, and keep Python where it earns its place, behind the API.
Python: pros and cons
Strengths
- Unmatched libraries for data, machine learning and scientific computing. No other language is close, and the gap is widening rather than narrowing as AI tooling accumulates here.
- Highly readable, which lowers maintenance cost, shortens onboarding and makes handover a genuine possibility rather than a fiction.
- Excellent as glue. It talks to C libraries, every serious database, every cloud provider’s API, every message broker and every file format you are likely to meet.
- Modern tooling has improved sharply. Type hints with mypy or pyright, ruff for fast linting and formatting, and lockfile-based dependency management bring real rigour to a dynamic language.
- Async support is mature enough for production. asyncio, plus FastAPI and async database drivers, handle high-concurrency I/O workloads well.
- A very large hiring pool that spans engineering, data and research, so a Python codebase is unusually easy to staff and to hand on.
Trade-offs
- CPython is slow for CPU-bound pure-Python code, commonly one to two orders of magnitude behind compiled languages on tight loops. This is a real property of the runtime, not a tuning problem, and the only durable answers are vectorising, offloading to compiled code, or choosing a different language for that component.
- The Global Interpreter Lock prevents true multi-core parallelism for Python bytecode within one process. Threads buy you nothing for compute-bound work. Free-threaded builds are progressing but are not yet the default reality for most production stacks, so the practical answer remains multiple processes, and that has to be a design decision rather than a retrofit.
- Packaging and environments remain a genuine friction point. Dependency resolution, transitive version conflicts, native wheels that do not exist for your architecture, and reproducible builds all still take deliberate care. uv and modern lockfiles have improved matters considerably, but this is the part of Python that most often bites teams who assumed it would be simple.
- Dynamic typing lets whole categories of error reach runtime. Without enforced type hints and static checks in CI, a large Python codebase drifts towards the state where nobody is confident changing anything, and the tests are the only specification.
- Memory usage is high relative to compiled languages. Every object carries overhead, and data-heavy processes can consume far more RAM than the size of the data suggests, which shows up as a cloud bill rather than an error.
- Deployment is heavier than a single binary. You are shipping an interpreter, a dependency tree and often native libraries, which is why containerisation is effectively mandatory and why images need attention to stay lean.
- The ecosystem’s breadth is also its supply-chain surface. A typical project pulls in a large transitive dependency tree from a public index, and treating that casually is how compromised or abandoned packages end up in production.
How we structure Python systems
We separate the parts of a Python system that must be fast from the parts that must be flexible, and we make that separation explicit rather than emergent. Business logic, orchestration and I/O stay in idiomatic Python where readability pays. Numerically heavy inner loops run inside NumPy operations, a compiled extension, or a dedicated service with a clear interface. The failure mode we are designing against is the system where a slow function is buried three layers inside a request handler, because by the time you find it the fix touches everything.
Concurrency decisions follow the interpreter’s constraints rather than fighting them. I/O-bound work uses asyncio or threads, because waiting does not hold the lock. CPU-bound work uses multiple processes, a task queue with process-based workers, or libraries that release the lock while they compute. Choosing this at design time costs an afternoon. Discovering it during a load test costs a rewrite, and we have seen enough teams take the second route to insist on the first.
For web services we lean on FastAPI when the workload is API-shaped and I/O-bound, and Django when the application wants an ORM, migrations, permissions and an admin out of the box. In either case we keep a firm boundary between the framework, the domain logic and the data access layer. Domain functions should be callable from a request handler, a Celery task, a CLI command and a test without knowing which is which, and that discipline is what lets a Python codebase change shape later without a rewrite.
Deployment is containerised as a matter of course, because shipping an interpreter plus a native dependency tree any other way is asking for an environment difference that only appears in production. Dependencies are locked, images are built in CI and kept lean, configuration comes from the environment, and secrets never live in the repository. None of this is exotic. It is simply the difference between a Python system you can redeploy with confidence and one nobody dares touch on a Friday.
Being honest about speed
CPython executes pure-Python bytecode slowly. On tight numerical loops it is commonly one to two orders of magnitude behind C, and anyone who tells you otherwise is selling something. That is the starting fact, and the way you live with it is to spend almost no time in pure Python on hot paths. Vectorise with NumPy so the loop runs in compiled code. Use pandas or Polars operations that execute natively rather than applying a Python function row by row. Push genuine bottlenecks into Cython, a native extension or Rust. The pattern is always the same: keep the interpreter for coordination and let compiled code do the arithmetic.
Profiling comes before optimising, always. In most data and web systems we are asked to look at, the time is not in the interpreter at all. It is in serialisation, in unindexed database queries, in chatty calls to a third-party API, in loading far more data than the answer requires, or in a pipeline that recomputes yesterday every night. Optimising Python code in those systems produces a rounding error. Fixing the query or the data volume produces a step change, and the only way to know which you are facing is to measure with cProfile, py-spy and real query timings against production-shaped data.
Memory deserves the same scrutiny as CPU, and gets far less. Python objects carry substantial per-object overhead, and a dataframe pipeline can hold several copies of the same data at once without anything looking obviously wrong. We watch peak memory rather than average, stream where a dataset does not need to be resident, choose column types deliberately, and reach for out-of-core or columnar engines when the working set stops fitting comfortably. Cloud instances are priced on the peak, not the mean.
For workloads that are irreducibly CPU-bound and latency-critical, real-time signal processing, high-frequency paths, heavy per-request computation with strict tail-latency budgets, we will recommend a different language for that component and keep Python around it for everything else. That is not a defeat, it is the correct architecture, and being willing to say it is part of what you are hiring us for.
Security in Python systems
Most Python security risk lives in the dependency tree rather than in your own code. The ecosystem’s greatest strength, that a library exists for everything, is also its largest attack surface, because a modest project routinely resolves to hundreds of transitive packages from a public index. We pin and lock versions, scan continuously with tools such as pip-audit and the equivalents wired into CI, keep a genuine inventory of what is installed, and treat an unmaintained dependency in a critical path as a risk to schedule work against rather than something to notice after a disclosure.
At the application boundary we rely on frameworks that behave safely by default and then verify that we have not undone them. Parameterised queries through the ORM or the driver rather than string-built SQL. Pydantic and FastAPI’s typed validation to reject malformed input at the edge instead of deep inside the domain. Framework-level CSRF and output escaping in Django. Authentication and authorisation checked at the object level rather than inferred from a URL. Secrets injected from a managed store, never committed, never printed into logs or error payloads.
The language has specific sharp edges we treat as non-negotiable rules. Never call eval or exec on anything derived from user input. Never unpickle data you did not produce yourself, because pickle deserialisation is arbitrary code execution by design and is a recurring source of serious incidents in ML systems that move models around as pickles. Be deliberate with YAML loading, subprocess invocation and path handling from untrusted sources. These are unglamorous rules, and they close off a surprising proportion of real-world Python compromises.
For systems handling personal or regulated data we add the surrounding discipline: least-privilege database credentials, encryption in transit and at rest, retention policies that actually delete things, audit logging of access to sensitive records, and care that training data and prompt logs in AI systems do not quietly become an uncontrolled copy of your customer database. UK and EU data protection obligations apply to the pipeline just as much as to the application in front of it, and pipelines are where people forget.
Scaling Python
Python scales horizontally well and vertically poorly, and every sensible architecture decision follows from that sentence. Because the interpreter lock caps what one process achieves on multiple cores for compute-bound work, the standard shape is many stateless worker processes behind a load balancer, scaled by adding more of them. Containers and orchestration make this straightforward, and it has the pleasant side effect of forcing statelessness, which is good design regardless of language.
Background and CPU-heavy work goes to process-based workers and a task queue, Celery, RQ, Arq or a cloud-native equivalent, rather than to threads inside the web process. This keeps request latency insulated from batch work, lets you scale the two independently, and means the lock is never the ceiling on throughput. It also gives you retries, dead-lettering and visibility into work that failed, which is exactly what you want the first time an upstream provider has a bad afternoon.
I/O-bound services scale on a completely different axis. A single async process can hold a very large number of concurrent connections, so the constraint moves off Python entirely and onto the database connection pool, the downstream service or the network. We size pools deliberately, because the most common failure we see in scaled Python services is not the application at all, it is a thousand workers each opening connections to a Postgres instance that will accept a few hundred. A pooler such as PgBouncer usually earns its keep long before a bigger database does.
Data workloads scale by moving computation to where the data is. Pushing aggregation into the database or warehouse, using columnar formats and partitioning so a job reads only what it needs, and processing incrementally rather than rebuilding history nightly. Distributed frameworks are available when a workload genuinely exceeds one machine, but we reach for them later than most, because a single well-provisioned node with an efficient engine handles far more than teams expect, and a distributed system you did not need is a permanent operational tax.
Python integrations & ecosystem
The technologies we most often pair with it. Each links to how we work with it.
How we work
We begin by deciding what genuinely needs to be Python and what does not, because that single decision shapes the cost of everything afterwards. Then we build the smallest thing that proves the approach against real data: one pipeline that actually runs end to end, one endpoint that actually serves a real model, one automation that actually replaces a manual process. Breadth comes after the approach is validated. For data and ML work especially, where the requirements often cannot be written down until someone has looked at the data, this ordering saves a great deal of expensive rework.
From day one we treat Python as production software rather than as scripting that got out of hand. Type hints checked in CI, ruff for linting and formatting, pytest with tests that run against realistic data rather than toy fixtures, locked dependencies and reproducible container builds. The dynamic nature of the language makes this discipline structural rather than optional. A Python codebase without it is fine for six months and unmaintainable at eighteen, and we have been called in to enough of the second kind to insist on the first.
Notebooks are where we explore, not where we ship. Analysis and experimentation belong in a notebook, and we use them freely for exactly that. But production code lives in modules, under version control, with tests and review, and the transition from one to the other is a deliberate step in every engagement rather than a thing that never quite happens. The most common inherited problem we see in Python data teams is a critical process that only runs when one person executes cells in the right order.
Because we operate what we build, observability is part of the definition of done. Structured logs, metrics on the things that actually indicate health, alerting on symptoms rather than causes, and for data systems, data quality checks that fail loudly instead of writing plausible nonsense downstream. Silent wrongness is the characteristic failure mode of data pipelines, and it is the one that damages trust in a system permanently.
The service behind it
Delivered throughCustom Software DevelopmentWhat we build with Python
The disciplines this technology most often shows up in, from a first build to taking over and stabilising an existing one.
How we deliver
- 01
Discover
We map the system, the constraints and the business it serves, including the parts nobody documented.
Architecture brief
- 02
Architect
Decisions get made, written down and defended before a line of production code exists.
Decision records
- 03
Build
Short cycles against working software. You see progress in the product, not in a status deck.
Shipping increments
- 04
Operate
Monitoring, incident response and iteration. The system is alive, so the engagement is too.
Runbooks & SLOs
Weighing up Python?
A short call with engineers who build in it and operate the result. If Python is the wrong tool for what you are doing, we would rather tell you now than bill you later.
Industries we use Python in
Domain knowledge changes what gets built. A few of the sectors we know before the first meeting.
Also in Languages
Why teams choose us for Python
We operate what we build
The people who write your Python also run it, so the design accounts for deployment, memory, failure and the three in the morning page rather than the happy path in a notebook. Operability is a design input here, not a phase at the end.
Senior engineers only
You get people who know when to reach for a compiled extension, exactly how the interpreter lock will bite your design, and what a dependency tree costs over three years. That judgement is the product. It comes from having shipped this before and having been wrong about it before.
Honest about trade-offs
We will tell you plainly when a component belongs in Go or Rust, when your performance problem is architectural rather than something a faster language would fix, and when the model you want is a worse answer than a well-indexed query. Saying so early is cheaper for you than discovering it late.
Data discipline, not just code discipline
Most Python systems we inherit fail quietly rather than loudly, producing plausible wrong numbers for weeks. We build validation, reconciliation and alerting into pipelines as standard, because trust in a data system is very hard to earn back once it has gone.
Typical timeline
- 01
Discovery
One to two weeks. Understand the data, the volumes, the constraints and the existing estate, and decide honestly where Python is and is not the right tool. Ends in an architecture, a plan and a cost, not a slide deck.
- 02
Prototype
A working vertical slice against real data. One pipeline that runs, one model served, one automation doing the job by hand today. The point is to find the ugly parts of the data early, because they are always there and they always change the plan.
- 03
Build
The full system in reviewed increments, with typing, tests, observability and reproducible deployment from the start rather than added later. Each increment is something you could run if you had to.
- 04
Hardening
Profiling against production-shaped volumes, memory and cost review, dependency and security scanning, failure-mode testing, and the runbooks somebody will need at three in the morning.
- 05
Operate
We run what we built. Monitoring, data quality checks, dependency updates, and tuning as data volumes and usage grow. Or a clean handover with documentation aimed at whoever inherits it, if that is what you prefer.
How pricing works
- A paid discovery engagement comes first on anything non-trivial. We look at the data, the constraints and the existing systems, and produce an architecture and a costed plan you own outright. It is deliberately short and deliberately not free, because the analysis is the valuable part and doing it properly is what makes the estimate worth anything.
- Fixed-scope pricing for well-defined builds. A specific pipeline, a serving endpoint, an automation with clear acceptance criteria. Where the requirements are genuinely knowable up front, you should not be carrying the estimation risk on our behalf.
- A monthly senior engagement for open-ended data, ML and platform work where the shape of the problem is still being discovered, which is the honest description of most machine learning work. Scope is reviewed each cycle, you see what was delivered, and you can stop.
- Ongoing operation and improvement for live systems, covering monitoring, dependency and security upkeep, tuning as data volumes grow, and continued iteration. Python systems in particular need somebody who owns the dependency tree over time.
- Third-party costs, cloud infrastructure, model and API usage, managed databases, commercial licences, are billed to your own accounts at cost with no mark-up from us. You keep the contracts, the credentials and the ability to walk away with the system running.
Hire Python engineers
Need Python capacity on your own team? We embed named senior engineers into your existing team (reporting to your leads, working in your rituals), so you add capacity without a hiring cycle.
Hire Python engineersCommon questions
Is Python too slow for our system?
It depends entirely on where the time actually goes, and that is a measurement, not an opinion. If your work is I/O-bound or backed by NumPy and pandas, Python is fast enough and the interpreter is almost never the bottleneck. If you have tight CPU-bound loops in pure Python, then yes, those need vectorising, a compiled extension or a different language for that specific component. In most systems we profile, the time turns out to be in queries, serialisation or data volume rather than in Python at all, which means the fix is cheaper than the client feared and in a different place than they expected.
What is the GIL and does it matter for us?
The Global Interpreter Lock means one Python process executes Python bytecode on a single core at a time. It matters enormously if you were planning to scale CPU-bound work with threads, because you cannot. The answer is multiple processes, a task queue with process-based workers, or pushing the work into libraries that release the lock while they compute. For I/O-bound work it is largely irrelevant, because waiting does not hold the lock. Free-threaded CPython is real progress but is not yet the default footing for most production stacks, so we design around the lock rather than hoping.
Should we use Django or FastAPI?
Django when you want a full framework: an ORM, migrations, authentication, permissions and a generated admin for a conventional application with a relational model at its heart. FastAPI when you are building typed, high-concurrency APIs and want async, automatic validation and generated documentation without the rest of a framework attached. They solve genuinely different problems, and running both in one estate is perfectly reasonable. We choose on the shape of your application rather than on fashion, and we will explain the reasoning rather than assert it.
Can we build our mobile app or website front end in Python?
No, and we would advise against trying. Python has no credible native mobile story and nothing that makes running it in the browser a sound production choice today. Use Swift or Kotlin, or a cross-platform framework, on the client, and a proper web technology for the front end. Keep Python for the back end, the data layer and the APIs, where it is genuinely excellent. Forcing it into the client produces something worse than the honest alternative and harder to hire for.
How do you keep a dynamically typed codebase maintainable?
Type hints checked by mypy or pyright in CI so they cannot silently rot, ruff for linting and formatting so style is never a review topic, real test coverage on the logic that matters, and locked reproducible environments so the code runs the same everywhere. Dynamic typing is a convenience during exploration and a liability in a large untyped codebase maintained by a changing team. On anything meant to last we treat static checking as standard practice from the first week, because retrofitting it onto a hundred thousand untyped lines is a project in itself.
Why is Python packaging still such a mess, and what do you do about it?
Because Python predates the conventions modern languages were designed with, and it carries decades of compatibility with native extensions, multiple packaging tools and platform-specific wheels. It has improved sharply, uv and lockfile-based workflows have removed a lot of the old pain, but it still needs deliberate attention. In practice we lock every dependency with hashes, build in containers so the environment is the artefact rather than a machine somebody set up once, pin the Python version explicitly, and treat a dependency upgrade as a change that goes through CI like any other. That turns packaging from a recurring surprise into routine maintenance.
We have models and analysis living in notebooks. Can you productionise that?
Yes, and it is a large part of what we do. The work is usually less about the model and more about everything around it: making the feature computation identical between training and serving, removing hidden state and manual cell ordering, handling the awkward data the notebook quietly skipped, adding validation so failures are loud, and putting the whole thing behind an interface with tests and monitoring. We keep the notebook as the place to explore, because that is what it is good at, and move the parts that need to run reliably into proper modules.
How do you handle the security of a large Python dependency tree?
By treating it as infrastructure rather than as something that happens when you type an install command. Every dependency is pinned and locked, the tree is scanned continuously in CI, and we keep an inventory of what is genuinely installed rather than what the requirements file suggests. We prefer fewer, better-maintained libraries over convenience packages that pull in half the index. Beyond that, we hold hard rules on the language’s sharp edges: no eval or exec on user input, and never unpickling data we did not produce, because pickle deserialisation is arbitrary code execution and is a recurring source of real incidents in machine learning systems that pass models around as files.
Building on Python?
Tell us what you are building and where it is stuck. A senior engineer reads it and gives you an honest read on whether Python is the right fit for the problem, or what we would reach for instead.
- 01A senior engineer reads it. Not a form queue, and not an account manager.
- 02We reply either with questions or with a straight answer that we are not the right fit.
- 03If it looks like a fit, a technical call with the person who would actually run the delivery.
- 04Then scope, effort and risk in writing, before anyone signs anything.