Python
Deploy Django, FastAPI or Flask. We pick Python 3.10 to 3.14 from your code, install with pip, uv, Poetry or Pipenv, and start your app on port 8080.
Updated 27 Oct 20264 min read
A Python app deploys from its requirements.txt, pyproject.toml or Pipfile. We choose Python, install your packages into the app's own virtual environment, and start Django, FastAPI or Flask for you. Your app listens on the port in the PORT variable, which is always 8080.
Versions
| Version | Notes |
|---|---|
| Python 3.14 | Supported |
| Python 3.13 | What new apps get when the code doesn't ask |
| Python 3.12 | Supported |
| Python 3.11 | Supported |
| Python 3.10 | No more security fixes from its makers: it still builds, with a warning |
We read the version from .python-version, runtime.txt (python-3.12), requires-python in pyproject.toml and python_version in Pipfile, and use our default, Python 3.13, when it meets all of them, otherwise the newest version that does. requires-python = ">=3.11" builds with 3.13; ">=3.10,<3.13" builds with 3.12. To stay on one version, write it in .python-version (3.12), choose it in the app's Set up and run settings, or set "version" in tanzahost.json.
How packages are installed
| Your code has | We run |
|---|---|
uv.lock | uv sync --frozen --no-dev |
poetry.lock | poetry install --only main |
Pipfile | Pipenv's requirements, installed with pip |
requirements.txt | pip install -r requirements.txt |
Only pyproject.toml or setup.py | pip install . |
Poetry, uv and Pipenv run at a fixed version. Packages that compile against a system library get it added for you, with the reason on the plan: psycopg2 without its binary wheel gets the PostgreSQL client library, mysqlclient the MySQL one, lxml libxml2 and libxslt, python-ldap the LDAP library. Everything goes into a virtual environment that's already active for the build, the app, its workers and its scheduled tasks. PYTHONUNBUFFERED=1 is set, so what you print shows in your logs at once.
How your app starts
| Framework | Start command |
|---|---|
A Procfile with a web: line | That line |
| Django | gunicorn yourproject.wsgi:application --bind 0.0.0.0:${PORT} |
| FastAPI | uvicorn main:app --host 0.0.0.0 --port ${PORT} |
| Flask | gunicorn main:app --bind 0.0.0.0:${PORT} |
| Anything else | python main.py, app.py or server.py |
For Django we read your project's name from manage.py. For FastAPI and Flask we look for the line that makes the app (app = FastAPI()) in main.py, app.py, server.py or api.py. When gunicorn isn't in your packages, we add it to the app's own environment for you; your files don't change. An app in a folder of the repository, like a template's backend, is found by itself.
Example: a Django shop
Asha's friend Neema moves her Django shop, project shop, to Tanzahost. Her repository has manage.py, requirements.txt with Django and psycopg2 (without the binary wheel), and .python-version with 3.12. The plan says: Django on Python 3.12, the PostgreSQL client library added for psycopg2, a PostgreSQL database made for it with its price first. She clicks Deploy. We install her packages, run collectstatic during the build and migrate before the version goes live, and start:
gunicorn shop.wsgi:application --bind 0.0.0.0:${PORT}Her variables are filled before the first deploy: SECRET_KEY made and kept encrypted, DJANGO_DEBUG=False where her settings read it, ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS set to her app's address, and DATABASE_URL from the database. A setting she already set is never replaced. See Connect an app to a database.
Before a new version gets visitors, our health check asks it for its health address at the app's private address, not at your domain. Django answers that with an error (400) when ALLOWED_HOSTS lists only your domains, so the new version never goes live. Either set ALLOWED_HOSTS to *, or keep your list of addresses and give the health check a path of its own, as DisallowedHost shows.
Django's migrate --noinput runs before each version goes live while Run migrations on each deploy is on (it is when we made the database). If it fails, the deploy stops and the version before keeps serving. See Deploying.
A Django app that uses SQLite moves to PostgreSQL by default, because a file database would be lost at every deploy. Choose Keep SQLite (one copy only) on the plan to keep the file in a kept folder instead.
Static files and uploads
collectstatic --noinput runs during the build. With WhiteNoise in your packages, Django serves them; otherwise they're served from STATIC_ROOT. Your MEDIA_ROOT is kept between deploys.
Workers and tasks
Celery, RQ or any long-running script runs as a worker, like celery -A shop worker. When your code uses Celery, or a Procfile has a worker: line, the plan adds the worker (and Celery beat, when you use it) with its price, on by default. Jobs on a timetable run as scheduled tasks, like python manage.py clearsessions every night.