Last Updated: June 01, 2026
For some of your web apps you develop in python, you will want to run them on the cloud so that your script can run 24/7. For some of your smaller applications, you may want to find the right free python hosting service so you don’t have to worry about the per month charges. These web applications might be a website written in flask, or using another web framework, it might be other types of python apps that runs in the background and runs your automation. This is where you can consider some of the hosting services that have a free plan and are still very easy to setup.
To find the right hosting platforms that fits your needs, you want to consider a few things:
- Ease of access to upload projects
- What type of support they provide
- What specifications that virtual server environment has to offer
One such new platform is called deta.sh. Deta is a free hosting service that can be used to provide web hosting for deploying python web applications or other types of python applications that run in the background.
The deta service, as of mid-2022, is still in the development stage and is expected to have a permanent free python hosting service so that online python applications can be setup and deployed quickly and easily. Deta is a relatively new service but is a service that is intended to compete with pythonanywhere, heroku, and similar services to run python on web servers. The service lets you host python script online without fuss directly from a command line, much like how you can check in code to github. Although it is new, it has the potential to be one of the best free python hosting there is in order to get your python online.
The platform provides you mini virtual environments (called ‘micros’) where you can host your python scripts. These can be separated into workspaces called ‘projects’ so that you can also more easily manage your environments. The way you can access/upload your code is with the command line through a password Access Token.

We will go through step by step how to run your python online. For this article, we will guide you on using deta to host a simple flask based web page so that you can have python as a webserver.
Python developer and educator with 15+ years building production systems across data engineering, web APIs, and AI tooling. Founder of Python How To Program — 270+ in-depth tutorials covering the modern Python stack.
Signing up for Deta.sh
Deta.sh is effectively a cloud python hosting service which sits on top of AWS and allows you to deploy your python code into a virtual machine (called a deta micro), store files (called data drive) and also store data (called deta base). Unlike AWS or other hosting services, you can quickly host and run your script without going through the hassle of setting up server, security configurations etc.
The Deta.sh team offers the service for free in order to allow developers to monetize the solutions where deta.sh will be able to share some of that revenue. To date, there are no paid Deta.sh hosting plans for python hosting and no intention. So you can continue to run python code online forever.
To begin with, head over to the website https://deta.sh to first create an account.

Once you have submitted, go to your email and click on the verify link.

After you click on sign-in, enter the same username and password, and you will be taken to the default page where you will have the ability to “See My Key”

Click on the “See My Key” to see your secret password. You will only be able to see it once and will not be able to see it ever again.
This is what they project key will look like:

You need both the key and the project id.
Think of the key like a password and the “Project ID” as a password. When you want to access your deta.sh to upload programs, make changes, you will need to use your project key to access your space.
If you lose your project id/key, you will not be able to recover it. However, you can create a new one with Settings->Create Key option.

One thing I’d like to call out is the Project ID. This is the ID of this particular s[ace

If you have multiple programs which access deta.sh, it is best to have separate project keys. The reason is that if one of your keys are compromised, then you can simply just change that key and not have all your applications be affected.
Setting Up Your Remote Access For Deta.sh
We will first setup deta.sh in the command line interface so that you can communicate to your deta.sh space on the cloud.
You can do this with either one of:
Mac / Linux:
curl -fsSL https://get.deta.dev/cli.sh | sh
Windows:
iwr https://get.deta.dev/cli.ps1 -useb | iex
Once that’s done, what will happen is that there will be a hidden folder called $HOME/.deta that is created (specifically in the case of Mac / Linux). It’s in this directory that the deta command line application will be found.
You can type deta --help to check that the command line tool was installed correctly

Next, you will need to create an access token so that you can connect to your deta.sh account. For this you will need to create an access token. Go to your deta.sh home page (e.g. https://web.deta.sh/) and then go back to the main projects page.

Next, click on the Create Access token under settings

Once you create token, this will create an Access Token so that you don’t need to login each time.

Copy this Access Token and then, create a file called tokens in the $HOME/.deta/ directory. Steps for Mac/Linux are:
cd $HOME/.deta
nano tokens
You can then add the following json inside the tokens file:
{
"deta_access_token": "<your access token created above>"
}
Finally, you can install the python library that will be used to access the deta components with the deta library.
pip install deta
Have a Free Python Hosting Flask on Deta.sh
To create an environment to host your python code and have python web hosting, you need to create something called a “micro“. This is almost like a mini virtual server with 128mb of memory but will not be running all the time. They will wake up, execute your code, and then go back to sleep. Deta.sh is not designed for long running applications with heavy computations (use one of the public cloud providers for that!). Also, each micro has its own python online cloud private access.
To begin with, you can use the command deta new --python <micro name>. The <micro name> is the name to label the mini-virtual name.

The above command will create a directory called flask_test with a python script called main.py

The default code in the main.py is:
def app(event):
return "Hello, world!"
At the same time, this code will be uploaded to deta.sh. If you go to the dashboard page https://web.deta.sh/ you will see a sub-menu under the Micro menu. You may need to refresh your browser if you had it open.

You will notice that there’s also a URL for this deta micro which is the end point where your application output can be accessed. Think of this simply as the console output.

If you encountered any errors, in the command line, you can type deta logs to get an output of any errors from the logs.
To make a more useful application, we can create a flask application to show a more functional webpage. In order to do this, you will need to dell deta.sh to install the flask library. You cannot use pip install unfortunately, but instead you need to use the requirements.txt instead.
First, add flask into a requirements.txt file in your local directory. So your file should simply look like this:
#requirements.txt
flask
Then in your main.py code file, you add the following, again this is in your local directory
from flask import Flask
app = Flask(__name__)
@app.route('/', methods=["GET"])
def hello_world():
return "Hello Flask World"
# def app(event):
# return "Hello, world!"
In order to now upload the changes to your micro, you will need to run the command deta deploy. This will upload the files requirements.txt and updates to main.py into your micro.
deta deploy
When executed, this should upload the code and install the libraries:

Managing Flask Forms On Free Python Hosting
Now that we have a simple static web page, we can create a more complex example where there’s a form that can be submitted. Using the weather API from openweathermap API, we can show the weather for a given location.
To get the weather data, we need to install two libraries pyowm and datetime. Hence, this will need to be added to requirements.txt.
#requirements.txt
flask
pyowm
datetime
Then for the code, the following can be updated in the main.py:
from flask import Flask, request, jsonify
import pyowm, datetime
app = Flask(__name__)
@app.route('/', methods=["GET"])
def get_location():
return """<html>
<body>
<form action="weather" method="POST">
<input name="location" type="text">
<input type="submit" value="submit">
</form>
</body>
</html>"""
@app.route('/weather', methods=["POST", "GET"])
def get_weather():
api_key = '<your open weather map API ley>'
owm = pyowm.OWM( api_key ).weather_manager()
weather_data = owm.weather_at_place('Bangalore').weather
ref_time = datetime.datetime.fromtimestamp( weather_data.ref_time ).strftime('%Y-%m-%d %H:%M')
weather_str = f"<h1>Weather Report for: {request.form['location']}</h1>"
weather_str += f"<ul>"
weather_str += f"<li><b>Time:</b> { ref_time } </li>"
weather_str += f"<li><b>Overview:</b> {weather_data.detailed_status} </li>"
weather_str += f"<li><b>Wind Speed:</b> {weather_data.wind()} </li>"
weather_str += f"<li><b>Humidity:</b> {weather_data.humidity} </li>"
weather_str += f"<li><b>Temperature:</b> {weather_data.temperature('fahrenheit')} </li>"
weather_str += f"<li><b>Rain:</b> {weather_data.rain} </li>"
weather_str += f"</ul>"
return weather_str
# def app(event):
# return "Hello, world!"
Then to upload the code into deta.sh, you can use the command deploy:
deta deloy
Once deployed, you can then go to the website – this is the endpoint that was automatically generated by deta.sh above.

def get_location()Once submitted, then a call is made to OpenWeatherMap

/ url, then the function def get_weather() is called to process the form. The variable that was passed, can be access through request.form['location']. The above code works by first providing a form through the function def get_location() which generates a very simple form through HTML:
<html>
<body>
<form action="weather" method="POST">
<input name="location" type="text">
<input type="submit" value="submit">
</form>
</body>
</html>
When the submit button is pressed, the form calls the /weather URL with the field location. Once called, then the python function def get_weather() is called upon which a call to OpenWeatherMap.org is made to get the weather data for the given location.
Conclusion
This is just a tip of the iceberg of what you can do with deta. You can also run scheduled jobs, run a NoSQL database, and have file storage as well. Contact us if you’d like us to cover these areas too.
How To Use Python Decouple for Environment Variable Config
Intermediate
You write a FastAPI app that connects to a database and a third-party payment gateway. You hardcode the credentials during development and tell yourself you will fix it before deploying. A month later the repo is on GitHub and so is your production database password. This is not a hypothetical — it happens constantly, and it happens because os.environ.get() is clunky enough that developers avoid it until it is too late.
Python’s python-decouple library makes the right approach easier than the wrong one. It reads configuration from .env files or .ini files, applies type casting automatically, handles missing values with sensible defaults, and keeps your code clean. One pip install python-decouple is all it takes, and it works with any Python project — Flask, FastAPI, Django, or a plain script.
This article walks through everything you need to use python-decouple confidently: reading values, type casting, setting defaults, working with booleans, using .env vs .ini files, integrating with Django settings, and building a real-world config loader. By the end you will have a pattern you can drop into any project to keep secrets out of your source code for good.
Python Decouple: Quick Example
Here is a minimal working example that shows the core pattern — reading a string, an integer, and a boolean from a .env file — so you can see exactly what decouple does before we explore each feature in depth.
First, create a file named .env in your project root:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
PORT=8000
DEBUG=True
SECRET_KEY=my-local-dev-secret-key-change-in-prod
Now read those values in Python:
# quick_decouple.py
from decouple import config
# String (default type)
database_url = config('DATABASE_URL')
# Integer -- decouple casts automatically
port = config('PORT', cast=int)
# Boolean -- handles 'True', 'true', '1', 'yes', etc.
debug = config('DEBUG', cast=bool)
# String with a fallback default
secret_key = config('SECRET_KEY', default='fallback-dev-key')
print(f"DB: {database_url}")
print(f"Port: {port} (type: {type(port).__name__})")
print(f"Debug: {debug} (type: {type(debug).__name__})")
print(f"Key: {secret_key[:10]}...")
Output:
DB: postgresql://localhost:5432/myapp
Port: 8000 (type: int)
Debug: True (type: bool)
Key: my-local-d...
Notice that port comes back as a real Python int and debug as a real bool — not strings. With os.environ you would need to write int(os.environ['PORT']) and handle the conversion yourself every time. Decouple does that work once, at the point of reading, so the rest of your code receives properly typed values.
Read on to see how decouple handles missing values, search paths, .ini files, and real-world project layouts.
What Is Python Decouple and Why Use It?
Python decouple is a library that implements the Twelve-Factor App principle of strict separation between configuration and code. Configuration here means anything that is likely to vary between deployment environments: database URLs, API keys, feature flags, port numbers, and debug settings. The idea is that these values live in the environment (a .env file locally, environment variables in production), not in the source code that gets committed to a repository.
Think of it like a restaurant kitchen. The recipes (your code) are written down and shared. The ingredients (your config values) change depending on what the supplier has that day — and the head chef does not write the supplier’s phone number into every recipe card. They keep it in a separate contact file. Decouple is that contact file system for your Python app.
Decouple vs os.environ
Here is how python-decouple compares to using os.environ directly:
| Feature | os.environ | python-decouple |
|---|---|---|
| Read string value | os.environ['KEY'] — raises KeyError if missing | config('KEY') — raises UndefinedValueError with clear message |
| Default value | os.environ.get('KEY', 'default') | config('KEY', default='default') |
| Integer casting | int(os.environ.get('PORT', '8000')) | config('PORT', default=8000, cast=int) |
| Boolean casting | Manual: 'True' == os.environ.get('DEBUG') | config('DEBUG', cast=bool) handles True/true/1/yes |
| Read from .env file | Requires python-dotenv or manual parsing | Built in — searches parent directories automatically |
| Support .ini files | No | Yes — useful for projects with existing .ini configs |
| Test overrides | Must monkeypatch os.environ | Can pass values directly in code during tests |
The bottom line: os.environ is built-in and requires no extra dependency, but every type conversion is manual boilerplate. Decouple pays for itself the moment you have more than two or three config values that need casting.
Installing python-decouple
Install it with pip in your virtual environment:
# install_decouple.py (run this in your terminal, not as a script)
pip install python-decouple
Output:
Successfully installed python-decouple-3.8
There is one important naming note: the library is called python-decouple on PyPI (what you install), but the import name is decouple (what you use in code). Do not confuse it with decouple on PyPI — that is a different package for Django-specific use. Always install python-decouple.
The .env File: Format and Best Practices
A .env file is a plain text file with one KEY=value pair per line. Decouple searches for it starting in the directory of the script being run, then walks up to parent directories. This means you can place it at the root of your project and it will be found regardless of which subdirectory you run from.
# .env (place this in your project root)
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/myapp_dev
# Server
PORT=8000
HOST=0.0.0.0
# Feature flags
DEBUG=True
ENABLE_CACHING=False
# Third-party APIs
STRIPE_SECRET_KEY=sk_test_abc123
SENDGRID_API_KEY=SG.xyz789
# Email
EMAIL_BACKEND=console
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
There are a few formatting rules to know. Values do not need quotes — DEBUG=True works fine. If your value contains spaces or special characters, wrap it in single or double quotes: FULL_NAME='Ada Lovelace'. Lines starting with # are comments and are ignored. Empty lines are also ignored.
The most important rule: add .env to your .gitignore immediately. Create a .env.example file with the same keys but dummy values, and commit that instead. New developers clone the repo, copy .env.example to .env, fill in their local values, and they are ready to go.
Type Casting with cast=
Every value in a .env file is stored as a string. Decouple’s cast parameter converts the string to the type you need before returning it, so the rest of your code never sees a string where it expects an integer or boolean.
Integers and Floats
Pass cast=int or cast=float to convert numeric config values. This is far cleaner than wrapping every read in a manual conversion.
# cast_examples.py
from decouple import config
# These values exist in .env:
# PORT=8000
# WORKERS=4
# TIMEOUT=30.5
port = config('PORT', default=8000, cast=int)
workers = config('WORKERS', default=2, cast=int)
timeout = config('TIMEOUT', default=30.0, cast=float)
print(f"Port: {port} -- {type(port).__name__}")
print(f"Workers: {workers} -- {type(workers).__name__}")
print(f"Timeout: {timeout} -- {type(timeout).__name__}")
Output:
Port: 8000 -- int
Workers: 4 -- int
Timeout: 30.5 -- float
If the .env value cannot be cast to the requested type — for example, PORT=eight_thousand — decouple raises a ValueError with a clear message pointing to the offending key. You get the error at startup when reading config, not somewhere deep in your app when the value is used.
Booleans
Boolean config values are tricky with os.environ because every string is truthy. "False" evaluates to True in Python because it is a non-empty string. Decouple’s boolean cast handles this correctly by recognizing a set of canonical true and false values.
# cast_bool.py
from decouple import config
# .env contains:
# DEBUG=True
# ENABLE_CACHING=False
# USE_SSL=yes
# MAINTENANCE_MODE=0
debug = config('DEBUG', cast=bool)
caching = config('ENABLE_CACHING', cast=bool)
ssl = config('USE_SSL', cast=bool)
maintenance = config('MAINTENANCE_MODE', cast=bool)
print(f"DEBUG: {debug}")
print(f"ENABLE_CACHING: {caching}")
print(f"USE_SSL: {ssl}")
print(f"MAINTENANCE_MODE: {maintenance}")
Output:
DEBUG: True
ENABLE_CACHING: False
USE_SSL: True
MAINTENANCE_MODE: False
The recognized truthy values are True, true, 1, yes, on. The recognized falsy values are False, false, 0, no, off. Anything else raises a ValueError. This strict set prevents the bug where DEBUG=False still evaluates to True because you forgot to cast.
Comma-Separated Lists
Decouple does not have a built-in list type, but you can pass any callable as the cast argument — including a lambda that splits a string into a list.
# cast_list.py
from decouple import config, Csv
# .env contains:
# ALLOWED_HOSTS=localhost,127.0.0.1,myapp.com
# CORS_ORIGINS=http://localhost:3000,https://app.example.com
# Option 1: built-in Csv helper (strips whitespace, handles quoting)
allowed_hosts = config('ALLOWED_HOSTS', cast=Csv())
# Option 2: lambda for simple cases
cors_origins = config('CORS_ORIGINS', default='', cast=lambda v: [s.strip() for s in v.split(',')])
print(f"Allowed hosts: {allowed_hosts}")
print(f"CORS origins: {cors_origins}")
Output:
Allowed hosts: ['localhost', '127.0.0.1', 'myapp.com']
CORS origins: ['http://localhost:3000', 'https://app.example.com']
The Csv() helper from decouple is the cleaner option for comma-separated values. It handles edge cases like extra whitespace and quoted values with commas inside them. The lambda approach works fine for simple cases where you control the format.
Defaults and Missing Values
When a key is missing from both the .env file and the actual environment, decouple’s behavior depends on whether you provided a default.
# defaults_demo.py
from decouple import config, UndefinedValueError
# KEY_WITH_DEFAULT is not in .env -- returns the default
log_level = config('LOG_LEVEL', default='INFO')
print(f"Log level: {log_level}")
# KEY_WITH_NONE_DEFAULT is not in .env -- returns None
cache_url = config('CACHE_URL', default=None)
print(f"Cache URL: {cache_url}")
# KEY_REQUIRED is not in .env and has no default -- raises UndefinedValueError
try:
api_key = config('REQUIRED_API_KEY')
except UndefinedValueError as e:
print(f"Missing required config: {e}")
Output:
Log level: INFO
Cache URL: None
Missing required config: REQUIRED_API_KEY not found. Declare it as envvar or define a default value.
This behavior is intentional and useful. Required values — things your app absolutely cannot run without — should have no default. That way decouple raises a clear error at startup rather than letting the app start in a broken state and fail later with a cryptic message. Optional values should have a sensible default so the app can run in a minimal configuration without a full .env file in place.
.ini File Support
In addition to .env files, decouple can read from .ini files using the AutoConfig or explicit RepositoryIni approach. This is useful when your project already has a settings.ini or setup.cfg and you do not want to introduce a second config file.
# settings.ini
[settings]
DATABASE_URL=postgresql://localhost:5432/myapp
PORT=8000
DEBUG=True
# read_ini.py
from decouple import Config, RepositoryIni
# Explicitly read from a .ini file instead of .env
config = Config(RepositoryIni('settings.ini'))
database_url = config('DATABASE_URL')
port = config('PORT', cast=int)
debug = config('DEBUG', cast=bool)
print(f"DB: {database_url}")
print(f"Port: {port}")
print(f"Debug: {debug}")
Output:
DB: postgresql://localhost:5432/myapp
Port: 8000
Debug: True
The default config object (imported directly from decouple) uses AutoConfig, which searches for .env first, then .ini, then falls back to actual environment variables. You only need to use RepositoryIni explicitly when you want to force a specific file rather than letting decouple search.
Django Integration
Django’s settings.py is the most common place developers accidentally commit secrets. Decouple is designed to slot in cleanly as a drop-in replacement for hardcoded settings.
# settings.py (Django)
from decouple import config, Csv
# Core Django settings
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', cast=bool, default=False)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv(), default='localhost')
# Database -- dj-database-url makes this even cleaner
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME', default='myapp'),
'USER': config('DB_USER', default='postgres'),
'PASSWORD': config('DB_PASSWORD', default=''),
'HOST': config('DB_HOST', default='localhost'),
'PORT': config('DB_PORT', default=5432, cast=int),
}
}
# Email
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool, default=False)
# Stripe
STRIPE_PUBLIC_KEY = config('STRIPE_PUBLIC_KEY', default='')
STRIPE_SECRET_KEY = config('STRIPE_SECRET_KEY', default='')
The pattern is consistent throughout: use config('KEY') for required values that must exist in production, and config('KEY', default=...) for optional values with safe development defaults. The entire settings.py file becomes safe to commit because it contains no actual secrets — just the names of the keys and their defaults.
Real-Life Example: Environment-Aware FastAPI App
Here is a realistic FastAPI application config module that uses decouple to manage all its settings. This pattern — a dedicated config.py module that gathers all config into a dataclass — scales cleanly as the project grows.
# config.py
from dataclasses import dataclass
from decouple import config, Csv, UndefinedValueError
@dataclass
class AppConfig:
# Server
host: str
port: int
debug: bool
workers: int
# Database
database_url: str
# Security
secret_key: str
allowed_origins: list
# External APIs
stripe_secret_key: str
sendgrid_api_key: str
slack_webhook_url: str
# Feature flags
enable_caching: bool
enable_email: bool
def load_config() -> AppConfig:
"""Load and validate all application configuration at startup."""
return AppConfig(
# Server
host=config('HOST', default='0.0.0.0'),
port=config('PORT', default=8000, cast=int),
debug=config('DEBUG', default=False, cast=bool),
workers=config('WORKERS', default=1, cast=int),
# Database -- required in production, no default
database_url=config('DATABASE_URL'),
# Security -- required always
secret_key=config('SECRET_KEY'),
allowed_origins=config('ALLOWED_ORIGINS', cast=Csv(), default='http://localhost:3000'),
# External APIs -- optional with empty defaults (check before use)
stripe_secret_key=config('STRIPE_SECRET_KEY', default=''),
sendgrid_api_key=config('SENDGRID_API_KEY', default=''),
slack_webhook_url=config('SLACK_WEBHOOK_URL', default=''),
# Feature flags
enable_caching=config('ENABLE_CACHING', default=False, cast=bool),
enable_email=config('ENABLE_EMAIL', default=False, cast=bool),
)
# main.py
from fastapi import FastAPI
from config import load_config, AppConfig
cfg: AppConfig = load_config() # Fails fast at startup if required vars missing
app = FastAPI(debug=cfg.debug)
@app.get("/health")
def health():
return {
"status": "ok",
"debug": cfg.debug,
"caching": cfg.enable_caching,
"port": cfg.port,
}
if __name__ == "__main__":
import uvicorn
print(f"Starting on {cfg.host}:{cfg.port} (debug={cfg.debug})")
uvicorn.run(app, host=cfg.host, port=cfg.port, workers=cfg.workers)
Output (with example .env values):
Starting on 0.0.0.0:8000 (debug=False)
The key design choice here is that load_config() is called once at module level, so any missing required variable raises UndefinedValueError the moment the app starts — not on the first request five minutes into a production deploy. The dataclass gives you IDE autocomplete throughout the rest of the codebase and makes it obvious what configuration the app expects without opening the .env file.
Testing with Decouple
Config management and testability often conflict — tests need predictable values, but decouple reads from files. There are two clean approaches: use a test-specific .env file, or monkeypatch os.environ.
# test_config.py
import os
import pytest
from unittest.mock import patch
# Approach 1: patch os.environ before importing config
def test_debug_defaults_to_false():
with patch.dict(os.environ, {'DATABASE_URL': 'sqlite:///test.db', 'SECRET_KEY': 'test-key'}, clear=True):
# Reimport or reload config within the patch context
from decouple import config
# config() reads os.environ after checking .env
debug = config('DEBUG', default=False, cast=bool)
assert debug is False
def test_port_casting():
with patch.dict(os.environ, {'PORT': '9000'}):
from decouple import config
port = config('PORT', cast=int)
assert port == 9000
assert isinstance(port, int)
def test_missing_required_raises():
from decouple import config, UndefinedValueError
with patch.dict(os.environ, {}, clear=True):
# Remove DATABASE_URL from the environment
env_without_db = {k: v for k, v in os.environ.items() if k != 'DATABASE_URL'}
with patch.dict(os.environ, env_without_db, clear=True):
with pytest.raises(UndefinedValueError):
config('DATABASE_URL')
Output (pytest):
...
3 passed in 0.12s
The recommended approach for larger projects is to create a .env.test file and use a fixture that temporarily swaps the decouple search path to point at it. This gives you a full, realistic config setup for tests without polluting your development .env. The patch.dict(os.environ, ...)` approach shown above works well for unit tests of individual config values.
Frequently Asked Questions
Does decouple replace os.environ entirely?
Not entirely -- decouple reads from .env files first, then falls back to actual environment variables set in the shell or by the deployment platform. In production you typically do not deploy a .env file; instead, environment variables are set by the platform (Heroku config vars, Docker environment, Kubernetes secrets). Decouple reads those just fine through the os.environ fallback. The .env file is a development convenience, not a production requirement.
Where should I put my .env file?
Place it in the root of your project -- the same directory as manage.py (Django), main.py (FastAPI/Flask), or your top-level package. Decouple uses AutoConfig which walks up from the running script's directory until it finds a .env or .ini file, so as long as it is somewhere in the directory tree above your code, it will be found. Do not commit it to version control -- add it to .gitignore and commit a .env.example instead.
How do I handle multiple environments (dev, staging, prod)?
The cleanest approach is one .env per environment, kept out of the repo. Your CI/CD pipeline injects the appropriate values as environment variables for staging and production. Locally you maintain a .env with development values. You can also use a tool like direnv to switch .env files automatically when you change directories. Never create .env.production files and commit them -- that defeats the entire purpose.
What is the difference between python-decouple and python-dotenv?
Both libraries load .env files, but they take different approaches. python-dotenv loads values into os.environ as a side effect, making them available to os.environ.get() and any other code that reads the environment. python-decouple does not modify os.environ -- instead it provides a config() function that reads directly from the file or the real environment. Decouple is preferable when you want typed values, sensible defaults, and a clean API. Dotenv is useful when you need the values to land in os.environ for libraries that read from there directly.
Should I use .env files in production?
Generally no. Deploying a .env file to a server creates a file on disk containing secrets, which is a security risk if the server is ever compromised. In production, use your platform's secret management: Heroku config vars, AWS Secrets Manager, Docker secrets, Kubernetes secrets, or environment variables set in your deployment config. Decouple reads all of these through its os.environ fallback, so no code changes are needed between development (using .env) and production (using platform secrets).
How do I manage a large number of config values?
Group related config into separate config objects or dataclasses, as shown in the FastAPI example above. A single config.py module that defines everything in one place makes it easy to see what the app needs at a glance. Avoid calling config() scattered throughout your codebase -- centralizing config reads means you only have one place to look when a value is wrong, and one place to update when a key changes.
Conclusion
Python decouple solves a problem that trips up almost every developer at some point: configuration that leaks into source code. The library gives you config('KEY') with type casting, defaults, and clear errors for missing required values -- all reading from a .env file that stays out of your repository. We covered reading strings, integers, booleans, and lists; comparing decouple to raw os.environ; using .ini files; integrating with Django settings; and building a real FastAPI config module with a dataclass pattern.
The next step is to take the config module pattern from the real-life example and adapt it to your own project. Start by identifying every hardcoded string in your settings that could change between environments -- database URLs, API keys, debug flags, port numbers -- and move them behind config() calls. Your future self, and anyone else who has to deploy your app, will thank you.
Full documentation for python-decouple is at github.com/HBNetwork/python-decouple. The Twelve-Factor App methodology that inspired it is documented at 12factor.net/config. For a complementary approach using type-validated settings objects, see our guide on Pydantic Settings for Configuration Management. If you prefer a more flexible multi-environment solution, dynaconf is worth exploring. To keep your secrets extra safe, pair decouple with the built-in Python secrets module for generating tokens and keys.
Related Articles
Further Reading: For more details, see the Python virtual environments documentation.
Frequently Asked Questions
Is Deta still free for hosting Python apps?
Deta Space offers a free tier for personal use. The original Deta.sh Micros service has evolved. For free Python hosting alternatives, consider Railway, Render, PythonAnywhere, or Google Cloud Run’s free tier.
What are the best free Python hosting alternatives?
PythonAnywhere offers a free tier for web apps. Render provides free static sites and web services. Railway has a free trial. Google Cloud Run and AWS Lambda have generous free tiers for serverless deployments.
How do I deploy a Python Flask app for free?
Use Render (connect GitHub repo), PythonAnywhere (upload directly), or Railway (deploy from GitHub). Each provides different advantages for hobby and small-scale projects.
What should I consider when choosing Python hosting?
Consider free tier limits, sleep/cold-start behavior, database availability, custom domain support, deployment method, Python version support, and scaling options.
Can I host a Python bot or script for free?
Yes. PythonAnywhere allows always-on tasks. Google Cloud Functions and AWS Lambda handle event-driven scripts. For Discord/Telegram bots, Railway and Render offer free tiers suitable for small bots.
Continue Learning Python
Tutorials you might also find useful: