Intermediate
A config file is a flat file but is used for reading and writing of settings that affect the behaviour of your application. These files can be incredibly useful so that you can put individual settings inside the human editable file and then have the settings read from your application. This helps you configure your application in the way you need without having to change the application code.
Typically the config file is edited by a simple text editor by the user, then the application runs and reads the config file. If there are any changes to the config file, normally (depending how the code is written), the application will then have to be restarted to take on the new settings.
Some of the considerations for using a config file as a “data store” includes:
- Setup: There’s no setup that is required for files. You should use one of the config management python libraries that are available to make it easier to manipulate config files.
- Volume: Size Small-ish file size (< 5-10mb)
- Record access: Does not require to search data within the file to extract just a portion of the records. You would load or save all the data in the file in one go
- Data Writes: Applications don’t generally write to a config file, but it can be done. Instead the config file is edited outside in a text editor
- Data formats: Normally the data would be a structured record based (such as comma separated value – CSV or tab delimited), or a more complex structure such as what you see in windows based .INI files or JSON format even
- Editability: You generally want to allow direct editing of the file by users
- Redundancy: There’s no inbuilt redundancy. If there is any failure (data corrupt, the server with the file fails), then you’re out of luck. You need to setup your own mechanisms (e.g. replicate file to another server automatically)
Code examples to read and write from config file using ConfigParse
Setting up a config file is actually not that much harder than simply creating a constants inside your application. Your main decision will be what type of configuration file format you’d like to use as there are quite a few to choose from. Here are some options and samples:
| File type | Example config file |
|---|---|
|
1. Simple text file which is tab-delimited Python Library = noneExample: below |
records_per_page 10 |
|
2. A properties file with key value pair Python Library = None |
#webpage display |
|
3. INI file format Python library: configparser |
[database] |
|
4. JSON file format Python library: json |
{ “records_per_page”:10, “logo_icon”: “/images/company_log.jpg”}
|
Example 1: Simple text file which is tab-delimited
You can see a full article on how to read a text file in our “Storing Data in Files in Python” article. The short version of open a tab delimited file is as follows:
Suppose you have a configuration file as follows where each row has two fields which is separated by a tab:
config_data.txt
records_per_page 10
logo_icon /images/company_log.jpg
You can load the data into a python dictionary like the following:
config = {}
file_handler = open('config_data.txt', 'r')
for rec in file_handler:
config.update( [ tuple( rec.strip().split('\t') ) ] )
file_handler.close()
print(config)
The output will be as follows:
{'records_per_page': '10', 'logo_icon': '/images/company_log.jpg'}
Some explanation may be required on the code though to make it easier to understand. Firstly, the for loop is used to read a record line by line. So each time the for loop iterates, it will read a line into the field rec until the whole file is read.
The following code is a little tricky, but the intent is to take the two columns in the tab delimited file and create a dictionary key value pair.
config.update( [ tuple( rec.strip().split('\t') ) ] )
It works by the following:
- It first removes the newline character from the end of the line (through
rec.strip()) - This will then return a string which is then split with
split()by the a tab characters (denoted by‘\t’) - The result of this is a two filed array which is then created into a tuple format
- The tuple is then put in a list and added to list with the
[]brackets - The dictionary
.update()method is used to finally add they key value pair
Example 2: A properties file with key value pair
If you have a fairly simple configuration needs with just a key-value pair, then a properties type file would work for you where you have <config name> = <config value>. This can be easily loaded as a text file and then the key-value be loaded into a dictionary.
Imagine this was the config file: config_data.txt
#webpage display
records_per_page =10
logo_icon =/images/company_log.jpg
The following code could easily load this configuration:
config = {}
with open('config_data.txt', 'r') as file_hander:
for rec in file_hander:
if rec.startswith('#'): continue
key, value = rec.strip().split('=')
if key: config[key] = value
print( config )
Here the code ignores any comment lines (e.g. the line starts with a ‘#’), and then string-splits the line by the ‘=’ sign. This will then load the dictionary ‘config’
Example 3: INI file format using ConfigParse
You can see a full article on how the ConfigParse library works in our earlier article. The short version is as follows.
Suppose you have a configuration file as follows:
test.ini
[default]
name = development
host = 192.168.1.1
port = 31
username = admin
password = admin
[database]
name = production
host = 144.101.1.1
You can then read the file with the following simple code:
import configparser
config = configparser.ConfigParser()
#Open the file again to try to read it
config.read('test.ini')
print( config['database'][‘name’] ) #This will output ‘production’
print( config['database'][‘port’] ) #This will output ‘31’. As there is no port under
# database the default value will be extracted
Example 4: Reading Config values from a JSON file
With JSON being so popular, this is also another alternative you could use to keep all your config data in. It is very easy to also load.
Assume your config file is as follows: config_data.txt
{
"records_per_page":10,
"logo_icon": "/images/company_log.jpg"
}
Then the following code can be used to bring these into a dictionary:
import json
file_handler = open('config_data.txt', 'r')
config = json.loads( file_handler.read() )
file_handler.close()
print(config)
Where the output would be:
{'records_per_page': 10, 'logo_icon': '/images/company_log.jpg'}
Summary
A config file is a great option if you are looking to store settings for your applications. These are usually loaded at the start of the application and then can be loaded into a dictionary which can then serve as a set of constants which your application can use. This will both avoid the need to hardcode settings and also allow you to change the behaviour of your application without having to touch the code.
How To Build CLI Apps with Python Click
Intermediate
Every serious Python developer eventually needs to build a command-line interface. Whether it is a deployment tool, a data processing script, or a developer utility, a well-designed CLI makes the difference between a tool your team actually uses and one that sits forgotten. Python’s standard argparse module works, but it is verbose — you write 20 lines of setup code before you handle your first argument. Click is the modern alternative: decorator-based, expressive, and composable, it cuts that boilerplate in half and adds features argparse simply does not have.
Click was created by the team behind Flask and follows the same philosophy: explicit is better than implicit, but explicit does not have to be painful. You decorate a Python function with @click.command() and @click.option(), and Click handles argument parsing, help text, type conversion, validation, and error messages automatically. Install it with pip install click.
This article covers everything you need to build production-quality CLI tools with Click: basic commands and options, arguments, type validation, prompts, multi-command groups (subcommands), progress bars, and output formatting. By the end, we will build a complete file management CLI that demonstrates all these features working together.
Click Quick Example
Here is a complete Click CLI that greets a user, with an optional count parameter:
# quick_click.py
import click
@click.command()
@click.option('--name', default='World', help='Who to greet.')
@click.option('--count', default=1, type=int, help='Number of greetings.')
@click.option('--loud', is_flag=True, help='Use uppercase.')
def greet(name, count, loud):
"""A friendly greeting command."""
for _ in range(count):
message = f"Hello, {name}!"
if loud:
message = message.upper()
click.echo(message)
if __name__ == '__main__':
greet()
Run it from the terminal:
$ python quick_click.py --name Alice --count 3
Hello, Alice!
Hello, Alice!
Hello, Alice!
$ python quick_click.py --name Bob --loud
HELLO, BOB!
$ python quick_click.py --help
Usage: quick_click.py [OPTIONS]
A friendly greeting command.
Options:
--name TEXT Who to greet.
--count INTEGER Number of greetings.
--loud Use uppercase.
--help Show this message and exit.
Click generated a complete help page automatically from the function’s docstring and decorator metadata. The --help flag, type validation, and default values all come for free.
Options vs Arguments
Click distinguishes between two kinds of inputs: options (named flags like --name Alice) and arguments (positional inputs like a filename). Options are optional by default; arguments are required by default.
| Feature | Option (@click.option) | Argument (@click.argument) |
|---|---|---|
| Syntax | --flag value | Positional: cmd value |
| Required | Optional by default | Required by default |
| Help text | Shown in --help | Shown in usage line |
| Best for | Configuration, flags | Primary inputs (files, names) |
# options_arguments.py
import click
@click.command()
@click.argument('filename') # Required positional arg
@click.option('--output', '-o', default='-', # -o is a short alias
help='Output file (default: stdout)')
@click.option('--lines', '-n', default=10,
type=int, help='Number of lines to show.')
@click.option('--verbose', '-v', is_flag=True,
help='Show extra information.')
def head(filename, output, lines, verbose):
"""Show the first N lines of FILENAME."""
if verbose:
click.echo(f"Reading {filename}, showing {lines} lines")
try:
with open(filename) as f:
for i, line in enumerate(f):
if i >= lines:
break
click.echo(line, nl=False)
except FileNotFoundError:
click.echo(f"Error: {filename} not found", err=True)
raise SystemExit(1)
if __name__ == '__main__':
head()
Run it as python options_arguments.py myfile.txt --lines 5 --verbose. The -o short alias for --output is defined right in the option decorator. Click handles both -o file.txt and --output file.txt automatically.
Types and Validation
Click converts option and argument values to the specified Python type and shows a helpful error if the conversion fails. Beyond basic types, Click has specialized types like click.Path for file paths and click.Choice for enumerated values.
# types_demo.py
import click
@click.command()
@click.argument('input_file', type=click.Path(exists=True, readable=True))
@click.option('--format', 'output_format',
type=click.Choice(['json', 'csv', 'text'], case_sensitive=False),
default='text', help='Output format.')
@click.option('--max-size', type=click.IntRange(1, 1000),
default=100, help='Max size (1-1000).')
@click.option('--scale', type=float, help='Scaling factor.')
def process(input_file, output_format, max_size, scale):
"""Process INPUT_FILE with validation."""
click.echo(f"Processing: {input_file}")
click.echo(f"Format: {output_format}")
click.echo(f"Max size: {max_size}")
if scale:
click.echo(f"Scale: {scale}")
if __name__ == '__main__':
process()
When you pass an invalid value, Click provides a clear error message:
$ python types_demo.py myfile.txt --format xml
Error: Invalid value for '--format': 'xml' is not one of 'json', 'csv', 'text'.
$ python types_demo.py nonexistent.txt
Error: Invalid value for 'INPUT_FILE': Path 'nonexistent.txt' does not exist.
click.Path(exists=True) validates the file exists before your function even runs. click.IntRange(1, 1000) ensures the integer is within bounds. These validations happen automatically and produce user-friendly error messages — no manual error handling needed.
Interactive Prompts and Confirmation
For destructive operations, you often want to confirm with the user. Click provides @click.confirmation_option(), @click.password_option(), and click.prompt() for interactive input collection.
# prompts_demo.py
import click
@click.command()
@click.option('--username', prompt='Username',
help='Your username.')
@click.option('--password', prompt=True,
hide_input=True, confirmation_prompt=True,
help='Your password.')
@click.option('--database', prompt='Database name',
default='mydb', show_default=True)
def setup_connection(username, password, database):
"""Set up a database connection."""
click.echo(f"Connecting to {database} as {username}...")
click.echo(f"Password length: {len(password)} chars")
# In a real app, you'd use these to create a connection
click.echo("Connection configured successfully!")
@click.command()
@click.argument('filename')
@click.confirmation_option(prompt='Are you sure you want to delete this file?')
def delete_file(filename):
"""Permanently delete FILENAME."""
import os
try:
os.remove(filename)
click.echo(f"Deleted: {filename}", err=False)
except FileNotFoundError:
click.echo(f"File not found: {filename}", err=True)
if __name__ == '__main__':
setup_connection()
Run python prompts_demo.py and Click interactively prompts for each required value. The password is hidden during input (no echo to terminal) and asks for confirmation. The @click.confirmation_option adds a yes/no prompt before any destructive action — and automatically processes -y or --yes flags to skip the prompt in automated scripts.
Multi-Command Groups (Subcommands)
Real CLI tools like git and docker use subcommands: git commit, git push, docker build, docker run. Click’s @click.group() decorator creates this structure cleanly. Each subcommand is just another decorated function.
# groups_demo.py
import click
@click.group()
@click.option('--debug/--no-debug', default=False,
help='Enable debug output.')
@click.pass_context
def cli(ctx, debug):
"""Project management tool."""
ctx.ensure_object(dict)
ctx.obj['DEBUG'] = debug
@cli.command()
@click.argument('name')
@click.option('--template', default='basic',
type=click.Choice(['basic', 'flask', 'fastapi']),
help='Project template.')
@click.pass_context
def create(ctx, name, template):
"""Create a new project."""
if ctx.obj['DEBUG']:
click.echo(f"[DEBUG] Creating {name} with template {template}")
click.echo(f"Creating project '{name}'...")
click.echo(f"Template: {template}")
click.echo(f"Done! Run: cd {name} && python main.py")
@cli.command()
@click.argument('name')
@click.pass_context
def delete(ctx, name):
"""Delete a project."""
if ctx.obj['DEBUG']:
click.echo(f"[DEBUG] Deleting {name}")
click.confirm(f"Delete project '{name}'? This cannot be undone.", abort=True)
click.echo(f"Project '{name}' deleted.")
@cli.command()
@click.pass_context
def list_projects(ctx):
"""List all projects."""
click.echo("Projects:")
for project in ['api-service', 'data-pipeline', 'dashboard']:
click.echo(f" - {project}")
# Register the list command with a different name
cli.add_command(list_projects, name='list')
if __name__ == '__main__':
cli()
Run it as:
$ python groups_demo.py --help
Usage: groups_demo.py [OPTIONS] COMMAND [ARGS]...
Project management tool.
Options:
--debug / --no-debug Enable debug output.
--help Show this message and exit.
Commands:
create Create a new project.
delete Delete a project.
list List all projects.
$ python groups_demo.py create myapp --template flask
Creating project 'myapp'...
Template: flask
Done! Run: cd myapp && python main.py
$ python groups_demo.py --debug create myapp
[DEBUG] Creating myapp with template basic
Creating project 'myapp'...
The ctx.pass_context pattern passes a shared context object through all subcommands. The --debug flag is defined on the group level and passed down through context — this is the Click pattern for global flags that affect all subcommands.
Real-Life Example: A File Processing CLI
Here is a complete, practical CLI tool for processing text files — counting words, searching for patterns, and converting case — with progress bars for large files.
# filetools.py
import click
import re
from pathlib import Path
@click.group()
def cli():
"""File processing toolkit."""
@cli.command()
@click.argument('files', nargs=-1, type=click.Path(exists=True), required=True)
@click.option('--words/--no-words', default=True, help='Count words.')
@click.option('--lines/--no-lines', default=True, help='Count lines.')
@click.option('--chars/--no-chars', default=False, help='Count characters.')
def count(files, words, lines, chars):
"""Count words/lines/chars in FILES."""
total_w, total_l, total_c = 0, 0, 0
for filepath in files:
content = Path(filepath).read_text()
w = len(content.split())
l = content.count('\n')
c = len(content)
total_w += w; total_l += l; total_c += c
parts = []
if lines: parts.append(f"{l:>8} lines")
if words: parts.append(f"{w:>8} words")
if chars: parts.append(f"{c:>8} chars")
click.echo(f"{' '.join(parts)} {filepath}")
if len(files) > 1:
click.echo(f"{'':->40}")
click.echo(f"{total_l:>8} lines {total_w:>8} words total")
@cli.command()
@click.argument('pattern')
@click.argument('files', nargs=-1, type=click.Path(exists=True), required=True)
@click.option('--ignore-case', '-i', is_flag=True, help='Case-insensitive.')
@click.option('--count-only', '-c', is_flag=True, help='Print match count only.')
def search(pattern, files, ignore_case, count_only):
"""Search for PATTERN in FILES."""
flags = re.IGNORECASE if ignore_case else 0
for filepath in files:
content = Path(filepath).read_text()
matches = [(i+1, line) for i, line in enumerate(content.splitlines())
if re.search(pattern, line, flags)]
if count_only:
click.echo(f"{len(matches):>5} {filepath}")
else:
for lineno, line in matches:
click.secho(f"{filepath}:{lineno}: ", nl=False, fg='cyan')
# Highlight the match in yellow
highlighted = re.sub(pattern,
lambda m: click.style(m.group(), fg='yellow', bold=True),
line, flags=flags)
click.echo(highlighted)
if __name__ == '__main__':
cli()
Run as:
$ python filetools.py count README.md
45 lines 312 words README.md
$ python filetools.py search "import" *.py --ignore-case
filetools.py:1: import click
filetools.py:2: import re
filetools.py:3: from pathlib import Path
The nargs=-1 pattern on FILES accepts any number of file arguments, like the Unix convention. click.secho() combines echo with styled output (colors). The --ignore-case short alias -i matches grep’s convention, making the tool feel natural to Unix users.
Frequently Asked Questions
When should I use Click instead of argparse?
Use Click for new CLI tools — it is less verbose and more composable. argparse is already in the standard library and requires no installation, so it is better for simple scripts that need zero dependencies. Click shines for multi-command CLIs with many options, complex validation, interactive prompts, and colored output. If you are building something beyond a simple script, Click’s developer experience wins decisively.
How does Click compare to Typer?
Typer is built on top of Click and generates Click CLI definitions from Python function type hints. If you use type annotations throughout your code, Typer reduces Click boilerplate further — you get options and arguments from type hints with no decorators. The trade-off: Typer adds a dependency and is less flexible than Click for complex CLI patterns. Click is more explicit; Typer is more magic. Both are excellent choices.
How do I test Click commands?
Click provides a CliRunner for testing. Use from click.testing import CliRunner; runner = CliRunner(); result = runner.invoke(my_command, ['--option', 'value']). The result object has exit_code, output, and exception attributes. This lets you test CLI behavior in pytest without spawning a subprocess, and it works with input prompts by passing input='yes\n' to invoke().
Can Click read options from environment variables?
Yes. Set auto_envvar_prefix='MYAPP' on the group, and Click automatically reads MYAPP_OPTION_NAME from the environment for any option not provided on the command line. You can also set it per-option: @click.option('--api-key', envvar='API_KEY'). This is the standard pattern for 12-factor applications where configuration comes from the environment.
How do I package a Click app as a proper CLI command?
Add an entry_points section to your pyproject.toml: [project.scripts] mytool = "mypackage.cli:main". After pip install -e ., running mytool in the terminal invokes your Click function directly. This is the standard way to distribute CLI tools on PyPI — users install your package and get the command available system-wide.
Conclusion
We covered the full Click toolkit: defining commands with @click.command(), options with @click.option(), arguments with @click.argument(), type validation with click.Path and click.Choice, interactive prompts, multi-command groups with shared context using @click.pass_context, and colored output with click.secho(). The file processing CLI showed how to compose these features into a tool that feels like a native Unix command.
From here, explore Click’s progress bar support (click.progressbar()), file path handling with lazy file opening, and the CliRunner for testing. Click’s plugin system also allows distributing CLI extensions as separate packages — the same pattern used by Flask extensions.
Official documentation: click.palletsprojects.com
Related Articles
Related Articles
- How To Manage Python Environment Variables With dotenv and os.environ
- How To Read and Write JSON Files in Python 3
- How To Split And Organise Your Source Code Into Multiple Files in Python 3
Further Reading: For more details, see the Python configparser documentation.
Frequently Asked Questions
What is the best way to store settings in Python?
For simple key-value settings, use INI files with ConfigParser. For nested data, use JSON or TOML. For environment-specific settings, use .env files with python-dotenv. The best choice depends on your complexity needs and whether non-developers will edit the settings.
How do I create a config file in Python?
Use ConfigParser to create INI files: instantiate the parser, add sections and key-value pairs with config['section'] = {'key': 'value'}, then write with config.write(open('config.ini', 'w')). For JSON, use json.dump().
Should I use environment variables or config files?
Use environment variables for sensitive data (API keys, passwords) and deployment-specific settings. Use config files for application-level settings that rarely change. Many projects combine both: a config file for defaults and environment variables for overrides and secrets.
How do I prevent config files from being committed to Git?
Add your config file names to .gitignore (e.g., config.ini, .env). Provide a config.example.ini template in the repository so other developers know what settings are needed without exposing actual values.
Can I use YAML for Python configuration files?
Yes. Install PyYAML with pip install pyyaml and use yaml.safe_load() to read YAML files. YAML supports nested structures, lists, and comments, making it more expressive than INI. However, it is not part of Python’s standard library.