What Are Environment Variables? Python Absolute Beginners Tutorial

Final Exam & Certification

Complete this tutorial and pass the 10-question final exam to earn a downloadable certificate of completion.

skip to exam

Every Python program you write runs inside an environment. That environment knows things your code doesn't — the username of whoever launched the script, the path to installed software, and any secrets or settings you've chosen to store there. Environment variables are how programs read that information without it ever appearing in the source code.

Think of environment variables as a set of named sticky notes that the operating system holds onto. Any program running on that machine can ask to read those notes. Python gives you a simple dictionary called os.environ that holds every variable currently set in the environment. You can read from it, write to it, and check whether a particular key exists — all without changing a single line of your application logic.

What Is an Environment Variable?

An environment variable is a key-value pair that lives at the operating system level, outside any specific program. The key is always a string (typically uppercase by convention), and the value is always a string too, even if it represents a number. When you start a Python script, the interpreter inherits a copy of the environment from the process that launched it — your terminal, a web server, a CI/CD runner, or whatever else called Python.

You have almost certainly seen environment variables without realizing it. On macOS and Linux, the variable PATH tells the shell where to look for executable programs. On Windows it is named the same. The variable HOME holds your home directory path. These are environment variables that the OS sets automatically. You can add your own alongside them.

Note

All environment variable values are strings. If you store PORT=8080, Python will give you the string "8080", not the integer 8080. You must convert the value yourself: int(os.getenv("PORT", "8080")).

The most important reason to use environment variables is to keep sensitive information out of your code. API keys, database passwords, and secret tokens should never appear in a Python file. If they do, anyone who can read your source code — including everyone on GitHub if the repository is public — can read your secrets. Environment variables provide a clean separation between configuration (which changes per environment) and code (which stays the same).

This separation is sometimes called the twelve-factor app principle for configuration: store config in the environment, not in the code. It means the same Python file can run in development on your laptop, in staging on a test server, and in production in the cloud, each with completely different credentials, and none of those credentials ever touch the source code.

Python gives you several distinct patterns for accessing environment variables, each suited to different situations. Beginners typically start with os.getenv and move to more structured approaches as their programs grow. The accordion below covers the full range — from the simplest single-line read to production-grade typed configuration.

When to use
When the variable is required and your program cannot continue without it. A missing key raises a KeyError immediately at the point of access, which is a useful early failure signal. Best combined with a startup validation pass (see below) so errors surface before any work begins.
Downside
Raises KeyError if the variable is not set, which crashes the program at the access site. If you have ten required variables, you will discover missing ones one at a time unless you validate them all upfront.
Best practice
Do not scatter os.environ['KEY'] calls throughout your codebase. Collect all required variable reads into a single config module or function at startup, validate them together, and raise a single descriptive error that lists every missing variable at once.
When to use
When the variable is optional, or when a safe fallback exists for local development. Examples: os.getenv('PORT', '8080'), os.getenv('LOG_LEVEL', 'INFO'). The default is returned as-is if the variable is absent, so it must be a string, not an int or bool.
Downside
A missing required variable silently returns None (or your default), which can propagate deep into your application and produce confusing errors far from the original missing variable. Never use os.getenv for required secrets without a follow-up None check.
Best practice
Use os.getenv for optional config with safe defaults. For anything required, pair it with an immediate assertion or collect into a startup validation list rather than letting None travel through your code undetected.
What it is
A dedicated config.py (or settings.py) file at the top of your project that reads all environment variables in one place, validates them, converts types, and exposes clean named constants. The rest of your codebase imports from config rather than calling os.getenv directly.
Why it is better
Eliminates scattered os.getenv calls across dozens of files. When a variable is renamed or removed, you change one file. Type conversions (int, bool) happen once. Missing variable errors surface at import time, before any request is processed.
Example
from config import DATABASE_URL, DEBUG, PORT — clean, testable, and easy to audit. This is the pattern used in most production Django and Flask projects.
What it is
from dotenv import dotenv_values reads a .env file and returns a plain dictionary without touching os.environ at all. Your application works directly with the dictionary instead of the global environment.
When to use
When you want to keep environment variables isolated from the OS environment — useful in tests (prevents test variables from leaking into subprocesses), in CLI tools that run as sub-commands, or when merging multiple config files: config = {**dotenv_values('.env.shared'), **dotenv_values('.env.secret'), **os.environ}. The rightmost source wins, so OS variables override file values.
Downside
Libraries and subprocesses that read os.environ directly will not see the values. Use load_dotenv() if you need values available to third-party code.
What it is
A @dataclass or regular class whose fields are populated from environment variables using a from_env() class method. Each field has a declared type, so type checkers (mypy, pyright) can verify that your code uses configuration correctly.
Why it matters
Raw os.getenv always returns str | None. A typed config object lets your IDE and type checker know that config.PORT is an int and config.DEBUG is a bool. Bugs from misusing a string where an int is expected are caught before runtime.
Real-world example
This approach is what libraries like pydantic-settings automate. It is worth learning the manual dataclass pattern first so you understand what those libraries are doing for you.
When it is acceptable
Only for values that are truly constant, non-sensitive, and never different between environments: a mathematical constant, a fixed UI label, or an API version string that never changes.
Why to avoid it for everything else
Hardcoded secrets appear in source code and in version history permanently. Rotating a leaked credential requires rewriting Git history, not just updating a config file. Hardcoded configuration also makes it impossible to deploy to multiple environments without code changes.
The hidden risk
Even a private repository is not safe: access may be granted to contractors, third-party tools, or CI services. Secret scanning tools routinely find credentials in repositories that were "always private." The cost of extraction is zero once access exists.

Reading Environment Variables in Python

Python's built-in os module is all you need to get started. Import it at the top of your file, then access os.environ, which behaves like a regular Python dictionary.

python
import os

# Dictionary-style access — raises KeyError if the key is missing
home_dir = os.environ['HOME']
print(home_dir)

# Safer access with a default value
port = os.getenv('PORT', '8080')
print(f"Server will run on port {port}")

# Check whether a variable exists before using it
if 'DATABASE_URL' in os.environ:
    print("Database URL found.")
else:
    print("DATABASE_URL is not set.")

The difference between os.environ['KEY'] and os.getenv('KEY') matters a lot in practice. The dictionary-style access is strict: if the key is absent, Python raises a KeyError and your program stops. os.getenv is lenient: it returns None by default, or whatever fallback you pass as the second argument. For optional settings like a debug flag or a port number, os.getenv with a sensible default is the right choice. For required secrets like an API key, consider using os.environ['KEY'] so a missing variable surfaces as an error right away rather than failing silently later.

Pro Tip

For required environment variables, validate them all at startup rather than waiting for individual functions to fail. Collect every missing variable into a list and raise a single descriptive error: this saves you debugging one missing key at a time in production.

You can also iterate over os.environ just like any other dictionary, though in most real programs you will be reading specific named variables rather than looping over all of them.

python
import os

# Validate required variables at startup
REQUIRED = ['API_KEY', 'DATABASE_URL', 'SECRET_KEY']

missing = [var for var in REQUIRED if not os.getenv(var)]

if missing:
    raise EnvironmentError(
        f"Missing required environment variables: {', '.join(missing)}"
    )

api_key = os.environ['API_KEY']
database_url = os.environ['DATABASE_URL']

The Centralized Config Module Pattern

Rather than scattering os.getenv calls throughout your project, a more maintainable approach is a dedicated config.py that reads and validates everything in one place. The rest of your code imports named constants rather than calling os.getenv directly, which means type checkers, tests, and IDE autocomplete all work properly.

python
# config.py — centralized configuration module
import os
from dotenv import load_dotenv

load_dotenv()

# Collect all missing required variables in one pass
_REQUIRED = ['DATABASE_URL', 'API_KEY', 'SECRET_KEY']
_missing = [key for key in _REQUIRED if not os.getenv(key)]
if _missing:
    raise EnvironmentError(
        f"Missing required environment variables: {', '.join(_missing)}"
    )

# Typed constants — convert once, use everywhere
DATABASE_URL: str = os.environ['DATABASE_URL']
API_KEY: str      = os.environ['API_KEY']
SECRET_KEY: str   = os.environ['SECRET_KEY']
PORT: int         = int(os.getenv('PORT', '8080'))
DEBUG: bool       = os.getenv('DEBUG', 'false').lower() == 'true'
LOG_LEVEL: str    = os.getenv('LOG_LEVEL', 'INFO').upper()
python
# app.py — import from config, never call os.getenv here
from config import DATABASE_URL, PORT, DEBUG

print(f"Running on port {PORT}, debug={DEBUG}")
print(f"DB: {DATABASE_URL}")

This pattern means that if a variable name changes, you update one file. All type errors are caught at import time. And when writing tests, you can override os.environ before importing config to inject test values cleanly.

Using a Dataclass for Typed Configuration

For more complex projects, wrapping config in a @dataclass gives you named fields with declared types, default values, and the ability to pass the config object around as a single dependency rather than importing individual constants everywhere.

python
from __future__ import annotations
import os
from dataclasses import dataclass
from dotenv import load_dotenv

load_dotenv()

@dataclass(frozen=True)
class AppConfig:
    database_url: str
    api_key: str
    secret_key: str
    port: int = 8080
    debug: bool = False
    log_level: str = 'INFO'

    @classmethod
    def from_env(cls) -> AppConfig:
        missing = [
            key for key in ('DATABASE_URL', 'API_KEY', 'SECRET_KEY')
            if not os.getenv(key)
        ]
        if missing:
            raise EnvironmentError(
                f"Missing required environment variables: {', '.join(missing)}"
            )
        return cls(
            database_url=os.environ['DATABASE_URL'],
            api_key=os.environ['API_KEY'],
            secret_key=os.environ['SECRET_KEY'],
            port=int(os.getenv('PORT', '8080')),
            debug=os.getenv('DEBUG', 'false').lower() == 'true',
            log_level=os.getenv('LOG_LEVEL', 'INFO').upper(),
        )

# Create once at startup and pass to the rest of your application
config = AppConfig.from_env()
frozen=True

frozen=True on the dataclass makes the config object immutable after creation. This prevents any part of your code from accidentally mutating configuration at runtime, which can cause difficult-to-reproduce bugs. If you need to override values in tests, create a new AppConfig instance directly rather than mutating os.environ.

spot the bug click the line that contains the bug

The function below tries to read a port number from an environment variable and use it as an integer. One line contains a bug. Click the line you think is wrong, then hit check.

1 import os
2
3 def get_port():
4 port = os.getenv('PORT', 8080)
5 return int(port)
6 print(get_port())
The fix: Change os.getenv('PORT', 8080) to os.getenv('PORT', '8080'). The second argument to os.getenv must be a string because environment variable values are always strings. When the variable is not set, the integer 8080 is returned directly, and int(8080) happens to work fine — but if PORT is set to something like "not_a_number", the inconsistency between the default type (int) and the live value type (str) will cause confusing behaviour and break type-checking tools. Always pass a string default.

Setting Variables and Using .env Files

You can set environment variables in Python during runtime by assigning to os.environ. This is useful when you need to pass a value to a subprocess you are about to launch. The assignment only lasts for the lifetime of your Python process — it does not permanently change anything in the system.

python
import os

# Set a variable for this process only
os.environ['APP_ENV'] = 'development'

# Verify it was set
print(os.getenv('APP_ENV'))  # development

# Delete a variable
del os.environ['APP_ENV']
print(os.getenv('APP_ENV'))  # None

In local development, setting variables in the terminal every time you open a new session is inconvenient. The standard solution is a .env file: a plain text file in the root of your project that lists variable names and their values, one per line. The python-dotenv library reads this file and loads the values into os.environ when your application starts.

bash
# .env file — never commit this to version control
DATABASE_URL=postgresql://localhost/mydb
API_KEY=sk-abc123supersecretkey
PORT=5000
DEBUG=true
python
# app.py
import os
from dotenv import load_dotenv

# Load variables from .env into os.environ
load_dotenv()

database_url = os.getenv('DATABASE_URL')
api_key      = os.getenv('API_KEY')
port         = int(os.getenv('PORT', '8080'))
debug        = os.getenv('DEBUG', 'false').lower() == 'true'

print(f"Connecting to: {database_url}")
print(f"Debug mode: {debug}")
Boolean Conversion Trap

Never write bool(os.getenv('DEBUG', 'false')). Any non-empty string — including "false" and "0" — is truthy in Python, so bool("false") returns True. The correct pattern is os.getenv('DEBUG', 'false').lower() == 'true', which only evaluates to True when the variable is explicitly set to the string "true".

Warning

Never commit your .env file to a Git repository. Add .env to your .gitignore immediately when you create the file. Share a .env.example file instead, which lists the variable names with placeholder values so teammates know what to create locally.

code builder click a token to place it

Build the correct Python statement to import and call load_dotenv from the python-dotenv library:

your code will appear here...
import os from load_dotenv dotenv environ
Why: The correct import is from dotenv import load_dotenv. The package is named dotenv (installed via pip install python-dotenv), and you import the load_dotenv function specifically. Using import os or import environ would not load a .env file.
Environment variable flow — values enter os.environ from the OS and from .env via load_dotenv(), then your script reads them at runtime.

Setting Variables in the Terminal

Before your Python script can read an environment variable, that variable has to exist in the environment. If you are not using a .env file, you set variables directly in the shell. The syntax differs by platform.

bash
# macOS / Linux — set for the current terminal session only
export API_KEY=sk-abc123supersecretkey
export PORT=5000

# Run your script — it will inherit the variables above
python app.py

# Remove a variable from the current session
unset API_KEY
powershell
# Windows PowerShell — set for the current session only
$env:API_KEY = "sk-abc123supersecretkey"
$env:PORT = "5000"

# Run your script
python app.py

# Remove a variable
Remove-Item Env:API_KEY
Session Scope

Variables set with export (bash) or $env: (PowerShell) only exist for the life of that terminal session. Open a new terminal window and they are gone. This is exactly why python-dotenv exists for local development — it removes the need to re-export variables every time you open a terminal.

On macOS and Linux you can make a variable permanent by adding the export line to your shell profile (~/.bashrc, ~/.zshrc, etc.), though this is not recommended for secrets because those files are easy to accidentally share. For secrets, always prefer a .env file or a dedicated secrets manager.

Environment Variables in Production

The .env file and python-dotenv are local development conveniences. In production — a cloud server, a container, a CI/CD pipeline — you do not use a .env file at all. Instead, you set environment variables through the platform's own configuration interface, and your Python code reads them with exactly the same os.getenv calls. The code does not change; only the source of the values changes.

Here is how the most common platforms handle it:

How to set
Use the platform dashboard's "Environment Variables" or "Config Vars" section, or the CLI: heroku config:set API_KEY=value. The platform injects the variables into your app's environment at runtime.
python-dotenv in production?
Not needed. load_dotenv() is safe to call in production — it simply does nothing if no .env file is found — but the platform variables are already in os.environ without it.
How to set
Pass variables at run time with docker run -e API_KEY=value ..., or reference a file with --env-file .env. In Docker Compose, use the environment: block or the env_file: key.
Key point
Never bake secrets into a Dockerfile using ENV directives. Those values end up in the image layers and are readable by anyone with access to the image.
How to set
Store secrets in the repository's "Secrets and Variables" settings (GitHub) or your CI platform's equivalent. Reference them in your workflow with ${{ secrets.API_KEY }} and they are automatically injected as environment variables in the runner.
Key point
CI secrets are masked in logs. You should still never print or log environment variable values directly in your application, because production logs are often stored and accessible to multiple people.
The Pattern Is Consistent

Your Python code reads environment variables the same way everywhere. What changes between development and production is only where the values come from: a .env file locally, and the platform's secrets or environment configuration in production. This consistency is the whole point of the pattern.

How to Use Environment Variables in Python

Follow these five steps to go from zero to a working, secure environment variable setup in any Python project.

  1. Import the os module

    Add import os at the top of your Python file. This is a built-in module — no installation needed. It provides os.environ and os.getenv, the two main tools for working with environment variables.

  2. Read an environment variable safely

    Use os.getenv('VARIABLE_NAME') to retrieve a value. If the variable might not always be set, supply a default as the second argument: os.getenv('PORT', '8080'). Remember that the returned value is always a string, so convert it if you need a different type.

  3. Create a .env file for local development

    In your project root, create a file named .env (note the leading dot). Add your variable definitions one per line in the format KEY=VALUE. Do not put spaces around the equals sign, and do not wrap values in quotes unless the library you are using explicitly requires it.

  4. Load the .env file using python-dotenv

    Install python-dotenv with pip install python-dotenv. At the very top of your application entry point, before any other code reads environment variables, add from dotenv import load_dotenv and then call load_dotenv(). This reads your .env file and populates os.environ with the values inside it.

  5. Add .env to your .gitignore

    Open or create a .gitignore file in your project root and add a line containing only: .env. Then commit a .env.example file that lists the required variable names with placeholder values, so anyone cloning the repository knows what to create.

The Twelve-Factor App methodology holds that configuration should live in the environment, not in the code — so the same codebase can run in development, staging, and production by simply swapping the variables. (12factor.net/config)

Python Learning Summary Points

  1. Environment variables are key-value pairs stored at the OS level, outside your Python code. Python reads them through the os module's os.environ dictionary or the os.getenv function.
  2. All environment variable values are strings. Always convert to the appropriate type (int, bool, etc.) in your Python code rather than expecting the OS to do it.
  3. Use os.environ['KEY'] when a variable is required (raises KeyError if missing) and os.getenv('KEY', 'default') when it is optional or has a safe fallback.
  4. Validate all required variables at startup in a single pass. Raise one descriptive EnvironmentError listing every missing variable rather than letting your program crash one missing key at a time.
  5. A .env file combined with python-dotenv is the standard pattern for managing secrets during local development. Never commit the .env file to version control.
  6. Setting os.environ['KEY'] = 'value' inside Python only affects the current running process. The change disappears when the script exits.
  7. Never use bool(os.getenv('FLAG')) to read boolean flags. Any non-empty string — including "false" — evaluates as truthy. Use os.getenv('FLAG', 'false').lower() == 'true' instead.
  8. The .env file and python-dotenv are for local development only. In production, set variables through your hosting platform's configuration interface. Your os.getenv calls work identically either way.
  9. In growing projects, consolidate all environment variable reads into a single config.py module. The rest of your code imports named constants from it rather than calling os.getenv in multiple files. This makes auditing, refactoring, and testing significantly easier.
  10. A config @dataclass with a from_env() class method gives you typed, immutable configuration. Use frozen=True to prevent accidental mutation at runtime. This is the manual equivalent of what libraries like pydantic-settings automate.
  11. dotenv_values() is an alternative to load_dotenv() that returns a plain dictionary without touching os.environ. Use it when you want isolated config, need to merge multiple env files, or are writing tests that should not leak variables into subprocesses.

Understanding environment variables is one of the first practical skills that separates a beginner Python program from something ready to ship. Once you separate configuration from code, you can run the same program in development, staging, and production without changing a single line — you just swap out the environment.

check your understanding question 1 of 7

Frequently Asked Questions

An environment variable is a named value stored in the operating system's environment, outside your Python code. Python reads these values at runtime using os.environ or os.getenv, making your program configurable without changing source code.

Use os.getenv('VARIABLE_NAME') to read an environment variable. It returns None if the variable is not set, or you can provide a default: os.getenv('PORT', '8080').

os.environ is a dictionary-like object that holds all environment variables. Accessing a missing key raises a KeyError. os.getenv is a function that returns None (or a supplied default) instead of raising an error, making it safer for optional variables.

A .env file is a plain text file that stores environment variable definitions in KEY=VALUE format. The python-dotenv library reads this file and loads the values into os.environ so your Python program can access them with os.getenv.

No. A .env file typically contains secrets such as API keys and passwords. Always add .env to your .gitignore file. Share a .env.example file instead that lists the required variable names with placeholder values.

Yes. You can set os.environ['VARIABLE_NAME'] = 'value' in your script. However, this only affects the current process and its children. The change does not persist in the terminal session after the script exits.

Hardcoded secrets appear in source code and version history, where anyone with repository access can read them. Environment variables keep secrets out of source code, allow different values per environment (development, staging, production), and make it easier to rotate credentials without changing code.

On macOS and Linux, use export VARIABLE_NAME=value in your terminal before running your Python script. On Windows PowerShell, use $env:VARIABLE_NAME = "value". Both methods set the variable for the current terminal session only. When you close the terminal, the variable is gone. For local development, a .env file with python-dotenv is more convenient than typing export every session.

No. In production, you set environment variables through your hosting platform's configuration interface — Heroku config vars, a Docker -e flag, GitHub Actions secrets, and so on. The platform injects the values into os.environ automatically, so load_dotenv() is not needed. Calling it anyway is harmless (it does nothing when no .env file exists), but the .env file itself should never be deployed to a production server.

If you use os.environ['KEY'] and the key is not set, Python raises a KeyError. If you use os.getenv('KEY'), Python returns None. For required variables, it is good practice to check the value early in your program and raise a clear error if it is missing.

Certificate of Completion
Final Exam
Pass mark: 80% · Score 80% or higher to receive your certificate

Enter your name as you want it to appear on your certificate, then start the exam. Your name is used only to generate your certificate and is never transmitted or stored anywhere.

Question 1 of 10