🌐
Typer
typer.tiangolo.com › environment-variables
Environment Variables - Typer
Typer can use environment variables as alternative values for command-line parameters.
🌐
Typer
typer.tiangolo.com › reference › parameters
Parameters - Typer - Tiangolo.com
For more information and an extended example, see the documentation [here](https://typer.tiangolo.com/tutorial/options/version/#fix-with-is_eager). """ ), ] = False, envvar: Annotated[ str | list[str] | None, Doc( """ Configure an argument to read a value from an environment variable if it is not provided in the command line as a CLI argument.
Author: fastapi
🌐
Readthedocs
config-ninja.readthedocs.io › en › v1.6.0 › typer › models.html
typer.models API documentation
f.prompt_required = prompt_required 394 self.hide_input = hide_input 395 self.count = count 396 self.allow_from_autoenv = allow_from_autoenv 397 398 399class ArgumentInfo(ParameterInfo): 400 def __init__( 401 self, 402 *, 403 # ParameterInfo 404 default: Optional[Any] = None, 405 param_decls: Optional[Sequence[str]] = None, 406 callback: Optional[Callable[..., Any]] = None, 407 metavar: Optional[str] = None, 408 expose_value: bool = True, 409 is_eager: bool = False, 410 envvar: Optional[Union[str, List[str]]] = None, 411 shell_complete: Optional[ 412 Callable[ 413 [click.Context, click.Paramet
🌐
PyTutorial
pytutorial.com › python-typer-envvar-usage-guide
PyTutorial | Python Typer Envvar Usage Guide
August 11, 2025 - Environment variables help keep secrets out of code. They make apps more secure and configurable. Typer integrates this seamlessly. Use them for API keys, database URLs, or any sensitive data. They also allow different configurations per environment. The simplest way is using the envvar parameter in options.
🌐
GitHub
github.com › fastapi › typer › discussions › 1149
Allow env variable only · fastapi/typer · Discussion #1149
import typer from click.core import ParameterSource from typing import Annotated def allow_only_env_var(ctx: typer.Context, param: typer.CallbackParam, value: str) -> str: if ctx.get_parameter_source(param.name) != ParameterSource.ENVIRONMENT: raise typer.BadParameter("only allowed as env var.") return value def main(name: Annotated[str, typer.Option(envvar="NAME", callback=allow_only_env_var)]): print(f"Hello {name}") if __name__ == "__main__": typer.run(main) Behavior: $ python cli.py Usage: cli.py [OPTIONS] Try 'cli.py --help' for help.
Author: fastapi
Top answer
1 of 5
18

I will suggest using pydantic.

From StackOverflow pydantic tag info

Pydantic is a library for data validation and settings management based on Python type hinting (PEP484) and variable annotations (PEP526). It allows for defining schemas in Python for complex structures.

let's assume that you have a file with your SSL and PORT envs:

with open('.env', 'w') as fp:
    fp.write('PORT=5000\nSSL=0')

then you can use:

from pydantic import BaseSettings

class Settings(BaseSettings):
    PORT : int
    SSL : bool
    class Config:
        env_file = '.env'

config = Settings()

print(type(config.SSL),  config.SSL)
print(type(config.PORT),  config.PORT)
# <class 'bool'> False
# <class 'int'> 5000

with your code:

env = Settings()

if not env.SSL:
    print("Connecting w/o SSL!")
if 65535 < env.PORT: 
    print("Invalid port!")

output:

Connecting w/o SSL!
2 of 5
8

The following solution offers both runtime casting to the desired types and type hinting help by the editor without the use of external dependencies.

Also check kederrac's answer for an awesome alternative using pydantic, which takes care of all of this for you.


Working directly with a non-Python dotenv file is going to be too hard, if not impossible. It's way easier to handle all the information in some Python data structure, as this lets the type checkers do their job without any modification.

I think the way to go is to use Python dataclasses. Note that although we specify types in the definition, they are only for the type checkers, not enforced at runtime. This is a problem for environment variables, as they are external string mapping information basically. To overcome this, we can force the casting in the __post_init__ method.

Implementation

First, for code organization reasons, we can create a Mixin with the type enforcing logic. Note that the bool case is special since its constructor will output True for any non-empty string, including "False". If there's some other non-builtin type you want to handle, you would need to add special handling for it, too (although I wouldn't suggest making this logic handle more than these simple types).

import dataclasses
from distutils.util import strtobool

class EnforcedDataclassMixin:

    def __post_init__(self):
        # Enforce types at runtime
        for field in dataclasses.fields(self):
            value = getattr(self, field.name)
            # Special case handling, since bool('False') is True
            if field.type == bool:
                value = strtobool(value)
            setattr(self, field.name, field.type(value))

This implementation can also be done with a decorator, see here.

Then, we can create the equivalent of a ".env.example" file like this:

import dataclasses

@dataclasses.dataclass
class EnvironmentVariables(EnforcedDataclassMixin):
    SSL: bool
    PORT: int
    DOMAIN: str

and for easy parsing from os.environ, we can create a function like

from typing import Mapping

def get_config_from_map(environment_map: Mapping) -> EnvironmentVariables:
    field_names = [field.name for field in dataclasses.fields(EnvironmentVariables)]
    # We need to ignore the extra keys in the environment,
    # otherwise the dataclass construction will fail.
    env_vars = {
        key: value for key, value in environment_map.items() if key in field_names
    }
    return EnvironmentVariables(**env_vars)


Usage

Finally, taking these things together, we can write in a settings file:

import os
from env_description import get_config_from_map


env_vars = get_config_from_map(os.environ)

if 65535 < env_vars.PORT:
    print("Invalid port!")

if not env_vars.SSL:
    print("Connecting w/o SSL!")

Static type checking works correctly in VS Code and mypy. If you assign PORT (which is an int) to a variable of type str, you will get an alert!

To pretend it's a dictionary, Python provides the asdict method in the dataclasses module.

env_vars_dict = dataclasses.asdict(env_vars)
if 65535 < env_vars_dict['PORT']:
    print("Invalid port!")

But sadly (as of the time of this answer) you lose static type checking support doing this. It seems to be work in progress for mypy.

Find elsewhere
🌐
Jacob Kaplan-Moss
jacobian.org › tags › typer
Typer - Jacob Kaplan-Moss
import typer cli = typer.Typer() @cli.command() def all(pinboard_token: str = typer.Option(None, envvar="PINBOARD_TOKEN")): ... @cli.command() def new(pinboard_token: str = typer.Option(None, envvar="PINBOARD_TOKEN")): ... if __name__ == '__main___': cli() But I wanted --pinboard-token to be an option on the main command, not on the subcommands.
🌐
Medium
medium.com › @connect.hashblock › 7-typer-cli-patterns-that-feel-like-real-tools-ecbe72720828
7 Typer CLI Patterns That Feel Like Real Tools | by Hash Block | Medium
September 28, 2025 - # deps: python-dotenv (optional) from dotenv import load_dotenv import os, typer load_dotenv() # reads .env if present @app.command() def ingest( url: Annotated[str, typer.Option(envvar="MYTOOL_API_URL")] = "https://api.example.com", token: Annotated[str, typer.Option(envvar="MYTOOL_TOKEN")] = typer.Option(..., help="API token"), ): # Precedence: CLI flag > env var > default if not token: raise typer.BadParameter("Set --token or MYTOOL_TOKEN") typer.echo(f"POST -> {url} (auth: ****)") Clear precedence reduces “why did it use the wrong endpoint?” incidents.
🌐
Jacob Kaplan-Moss
jacobian.org › til › common-arguments-with-typer
Common Arguments With Typer - Jacob Kaplan-Moss
import typer from types import SimpleNamespace cli = typer.Typer() @cli.callback() def main( ctx: typer.Context, pinboard_token: str = typer.Option(None, envvar="PINBOARD_TOKEN"), ): if not pinboard_token: print( "Missing pinboard token; pass --pinboard-token or set env[PINBOARD_TOKEN]" ) raise typer.Exit(1) ctx.obj = SimpleNamespace(pinboard_token = pinboard_token) @cli.command() def all(ctx: typer.Context): # do something with ctx.obj.pinboard_token @cli.command() def new(ctx: typer.Context): # do something with ctx.obj.pinboard_token if __name__ == "__main__": cli()
🌐
Jeeves
jeeves.sh › typer
Typer - jeeves!
from enum import Enum, auto import typer class GreetingStyle(Enum): """Style of the greeting.""" ENGLISH = 'en' ITALIAN = 'it' def hi( name: str = typer.Argument( ..., help='Name of the one we would like to greet.', envvar='NAME_TO_GREET', ), style: GreetingStyle = typer.Option( GreetingStyle.ENGLISH, help='Style of the greeting.', ), ): """Greet the user.""" greeting = { GreetingStyle.ENGLISH: 'Hello', GreetingStyle.ITALIAN: 'Buongiorno', }[style] typer.echo(f'{greeting} {name}!')
🌐
Typer
typer.tiangolo.com › virtual-environments
Virtual Environments - Typer
For Typer projects, I recommend using uv to manage the project, its dependencies, and its virtual environment.
🌐
Tiangolo
tiangolo.com › guides › environment-variables
Environment Variables - tiangolo's boring personal website
If you already know what "environment variables" are and how to use them, feel free to skip this · An environment variable (also known as "env var") is a variable that lives outside of the Python code, in the operating system, and could be read by your Python code (or by other programs as well)
🌐
Rahulpai
rahulpai.co.uk › smart-clis-with-typer.html
Smart CLIs with Typer - Rahul Pai
June 5, 2023 - You can tell typer to prompt for input if an argument was not provided.
🌐
GitHub
github.com › fastapi › typer › blob › master › docs › environment-variables.md
typer/docs/environment-variables.md at master · fastapi/typer
Typer, build great CLIs. Easy to code. Based on Python type hints. - typer/docs/environment-variables.md at master · fastapi/typer
Author: fastapi