Last Updated: June 01, 2026
- Introduction to Python 3 Configuration Files
- Setup of Python 3 ConfigParser
- File Format of configuration file
- Reading the configuration file from python code
- Read config from the config file using ConfigParser
- Changing the datatype of the configuration value from ConfigParser
- What to do if a value is not available from a configfile
- Conclusion
- Full Code: ConfigParser Example Code
- Reference
- Want to see more useful tips?
- Frequently Asked Questions
- Related Articles
Intermediate
Putting parameters in configuration files can take some extra effort at the start, but then can save you a lot of time and heartache in the future. We are all tempted to simply hardcode parameters directly into our code as we save precious time when we write code, but then doing this properly can take extra effort. Some of us at least create constants or store parameters in a variable, while others store them in a class variable to keep this even cleaner. Arguably the best option is store these in a configuration file. In this article you’ll learn the steps compulsory to use configuration files in python 3. It will be strictly according to the official documentation of python 3.
ConfigParser is the class used to implement configuration files in python 3. The main function of using these files is to write python programs which can easily be modified by end users easily. The main aspect of this article is to know about the complete implementation of configuration files. We will cover the three main aspects in this article which are Setup, File format and Basic API.
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.
Introduction to Python 3 Configuration Files
Configuration files can play a vital role in any program and its management. One of the popular approaches to separate code from configuration is to store these files in YAML, JSON or INI and not in .py format. One reason that .py files are not used is that Python 3 can be slower when it comes to reloading. You would need to restart the whole program if you stored your config in a python .py file. Also, the end user can modify the code at will if it is in .py format. Configuration files make it easier to modify or change the code. The data stored in configuration is to have separation so that the programmer can focus on code development and ensure that is clean as possible and the user only needs to touch the configuration file.
Setup of Python 3 ConfigParser
The class used to create configuration files is ConfigParser. This is a part of the standard python 3 library so no need to do any pip installation. We have to import it: “import configparser” to use it or there is another way of using it, it will work in both python2 and python 3, which is:
import configparser
File Format of configuration file
One convention that is used for the file format is to use the extension .ini (short for initial or initiation) but you can use the configuration based on your own or on clients preferences. There are different parts of configuration files.
- A configuration file consists of one or more sections.
- The section names are written in these delimiters [section name].
- The concept is similar to mapping. It consists of key-value pairs meaning there is a name of the configuration item (“key”) and the other the actual value of the configuration (“value”)
- Two operators are used to initialize or separate key-value pair assignment operator (=) or colon operator (:).
- You can even put in a comment using the # or ; prefix.
Example:
[default]
host = 192.168.1.1
port = 31
username = admin
password = admin
[database]
#database related configuration files
port = 22
forwardx11 = no
name = db_test
In the above configuration file example, we have two sections first is [default] and second is [database]. Each section has its own key-value pairs/entries like username = admin and name = db_test. So all of the key-value pairs belong to a given section, so it is easier to organise your configuration files. Finally the sentence with a prefix of # is for commenting
Reading the configuration file from python code
Now, we will talk about the method to read from the config file. As mentioned earlier, ConfigParser is the module/class used to create configuration files. First, ConfigParser object has to be initialized: config = configparser.ConfigParser(); The following are functions:
Initialization of ConfigParser
You can can initiate the configuration file with the following syntax. Here the variable “config” will contain all the values
config = configparser.ConfigParser()
Write to a Configuration file with ConfigParser
Although normally you normally edit to a configuration file in a text editor by hand, there are times where you want to programmatically write to a config file. For example, this could be to create a default config file which a user can then use as a basis to change or edit. You may also want to over-ride a config entry (after confirming with the user) that is erroneous.
Once the object is initialised, we can now write in it. There are ways through which we can initialize the section to write in the config file. We are going use the example mentioned above in file format. Let’s initialize the default section using dictionary.
Example:
config['default'] = {
"host" : "192.168.1.1",
"port" : "22",
"username" : "username",
"password" : "password"
}
Here, “default” is the name of the section (the part in the actual configuration file that had the square “[” and “]” brackets) and curly braces denote the start and end of a dictionary. Inside the dictionary are key-value pairs i.e. “host” is the key and “192.168.1.1” is the value separated by colon “:”
Now, let’s initialize the database section using empty dictionary and add the key-value pairs line by line.
Example:
config['database'] = {}
config['database']['port'] = "22"
config['database']['forwardx11'] = "no"
config['database']['name'] = "db_test"
Here, “database” is the name of the section and curly braces denote the same start and end of a dictionary. In this case, the dictionary is empty. Key-value pairs i.e. “port” is the key and “22” is the value separated by colon “=.” This method provides a lot more flexibility.
Here’s the full code so far:
import configparser
config = configparser.ConfigParser()
config['default'] = {
"host" : "192.168.1.1",
"port" : "22",
"username" : "username",
"password" : "password"
}
config['database'] = {}
config['database']['port'] = "22"
config['database']['forwardx11'] = "no"
config['database']['name'] = "db_test"
with open('test.ini', 'w') as configfile:
config.write(configfile);
After initializing the sections in config, you can now write it to a config file:
with open('test.ini', 'w') as configfile:
config.write(configfile);
Now, you will be able to see the file named test.ini created.
Read config from the config file using ConfigParser
The next step is to read the file which you just have created.
- The config file can be read by using read() method: config.read(‘test.ini’). This will read the test.ini file which you just created.
- If you want to print just the sections available in configuration file, method sections() can be used: config.sections().
- Next is getting the value of any key stored in the section. config[‘database’][‘name’]
This will give you the value which is “db_test” of the key called “name” stored in data_base section.
The following code will print out all the values stored against the keys in the default section using a for loop.
for key in config['default']:
print(config['default'][key])
Code:

Output:

Changing the datatype of the configuration value from ConfigParser
The datatype of the object of ConfigParser is string by default. This is fine for most situations, but then suppose you want to get a true/false value instead, or a number value to do maths operations. For this the string default may not work. We can typecast/covert the datatype of the object of configparser or the datatype of keys of section into any other type such as integer, float etc. In order to change the datatype of object, you have to covert it manually or by using getter methods. The best and the preferred way is to use getter methods.
There are three getter methods:
- getint();
- getfloat();
- getboolean();
Example: config['default'].getint('port')
getint() will covert the datatype of port key of section “default” into “integer”. If you use the typeof(); method on port then it will show integer type now.
There is another way of doing it:
Example: config.getboolean('data_base', 'forwardx11')
In this way, config file is invoking the getboolean() method and its takin two parameters as argument. The first is the name of the section and the other is the key whole value’s type will be changed.
What to do if a value is not available from a configfile
A fallback result can also be obtained. Fallback is the result obtained when the key or section we want to get isn’t available.
Example: config.get('default', 'database', fallback='not_database')
In this case, not_database will be returned if the “database” key isn’t available or the section default is not found.
Conclusion
We come to know about the setup i.e. importing the ConfigParser first to create configuration files. Next section was about the file format. There you can check about the basic syntax of creating a configuration file. It consists of sections and key-value pairs.
We played with the data types of keys in default and data_base sections. We can change datatypes using getter methods. Last but not the least, we studied about the basic api like write, read and about fallback.
Using configuration files is not difficult and can save a lot of time. So in your next coding work, take the extra few minutes to create a configuration file instead of hardcoding.
Full Code: ConfigParser Example Code
import configparser
config = configparser.ConfigParser()
#Set up default item for hosts using dictionary
config['default'] = {"host" : "192.168.1.1",
"port" : "22",
"username" : "username",
"password" : "password" }
#setup config item bytes
config['database'] = {}
config['database']['port'] = "22"
config['database']['forwardx11'] = "no"
config['database']['name'] = "db_test"
#Write default file
with open('test.ini', 'w') as configfile:
config.write(configfile)
#Open the file again to try to read it
config.read('test.ini')
#Print the sections
print(config.sections())
print( config['database']['name'] )
#Print each key pair
for key in config['default']:
print(config['default'][key])
#print the type of integer value
print (type (config['default'].getint('port')))
print( config.getboolean('database', 'forwardx11') )
#Print default value
print( config.get('default', 'databaseabc', fallback='not_database') )
Output:

Reference
https://docs.python.org/3/library/configparser.html
Want to see more useful tips?
How To Use Python rich-click for Styled CLI Help Pages
Intermediate
You build a Click CLI tool, run it with --help, and get a wall of monochrome text. Option names blur together, required flags look identical to optional ones, and your carefully written docstring sits there as a grey blob. Every professional CLI tool — from pip to ruff to aws — has structured, colored, scannable help pages. Yours can too, and it takes one line of code to get started.
rich-click is a thin wrapper around Click that replaces its built-in help formatter with one powered by the rich library. You keep everything you already know about Click — decorators, commands, groups, options — and your help output gains panels, colored option tables, and structured sections without touching your business logic. Install it with pip install rich-click.
This article covers the full rich-click toolkit: drop-in replacement usage, option grouping with panels, command grouping in groups, theming and color configuration, epilog formatting with Markdown, and a real-life project that ties everything together. By the end, you will be able to ship CLI tools whose help pages look like they came from a professional software team.
rich-click: Quick Example
The fastest way to see what rich-click does is to swap one import. Here is a standard Click CLI and its rich-click equivalent side by side.
# quick_richclick.py
import rich_click as click
@click.command()
@click.option("--name", required=True, help="Your name.")
@click.option("--count", default=1, show_default=True, help="How many times to greet.")
@click.option("--verbose", is_flag=True, help="Enable verbose output.")
def greet(name, count, verbose):
"""Greet a user by name.
Prints a greeting the specified number of times.
Use --verbose to see extra detail.
"""
for i in range(count):
if verbose:
click.echo(f"[{i+1}/{count}] Hello, {name}!")
else:
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
greet()
Output of python quick_richclick.py --help:
Usage: quick_richclick.py [OPTIONS]
Greet a user by name.
Prints a greeting the specified number of times. Use --verbose to see extra
detail.
+----------+------------------------------------------------------------------+
| Option | Description |
+----------+------------------------------------------------------------------+
| --name | Your name. [required] |
| --count | How many times to greet. [default: 1] |
| --verbose| Enable verbose output. |
| --help | Show this message and exit. |
+----------+------------------------------------------------------------------+
The only change is import rich_click as click instead of import click. Your decorators, options, and function logic are completely unchanged. rich-click intercepts Click’s help rendering at the formatter level and replaces it with a rich-powered layout — the rest of your CLI works exactly as before.
The real power comes when you start grouping options and commands. Read on to learn how to build structured, professional help pages that guide users through complex CLIs.
What Is rich-click and Why Use It?
rich-click is an open-source library by Dominic Davis-Foster that wraps Click’s help formatter with the rich rendering engine. It was built to solve a specific, common problem: Click’s default help output is functionally correct but visually flat. When a CLI has 20 options across 5 subcommands, a flat alphabetical list becomes unreadable. Users scroll past relevant flags, miss required arguments, and give up.
The library takes a non-destructive approach. It does not fork Click or change its internals — it subclasses Click’s formatter and renderer, which means every Click feature (callback validation, command groups, option types, context settings) continues to work. You can also mix standard Click and rich-click in the same project during a gradual migration.
| Feature | Click (default) | rich-click |
|---|---|---|
| Help output style | Plain monochrome text | Colored panels and tables |
| Option grouping | Not available | OPTION_GROUPS config |
| Command grouping | Not available | COMMAND_GROUPS config |
| Epilog formatting | Plain text only | Rich Markdown |
| Color themes | Not configurable | Full color customization |
| Migration effort | N/A | One import change |
| Runtime behavior | Unchanged | Unchanged |
Install both rich-click and rich (it is pulled in as a dependency automatically) with pip:
# In your terminal
pip install rich-click
Python 3.7+ and Click 7 or 8 are supported. No other setup is required.
Grouping Options into Panels
The most impactful feature of rich-click is option grouping. Instead of one flat list of every flag, you can organize options into labeled panels that match how users think about the tool. A CLI with database options, output options, and auth options should look like three distinct sections, not a single alphabetical dump.
Option groups are configured via click.rich_click.OPTION_GROUPS, a dictionary that maps command names to a list of group definitions. Each group has a name and a list of options (the flag names as strings).
# option_groups.py
import rich_click as click
click.rich_click.OPTION_GROUPS = {
"deploy": [
{
"name": "Connection",
"options": ["--host", "--port", "--timeout"],
},
{
"name": "Authentication",
"options": ["--token", "--username", "--password"],
},
{
"name": "Output",
"options": ["--format", "--verbose", "--quiet"],
},
]
}
@click.command()
@click.option("--host", default="localhost", show_default=True, help="Server hostname.")
@click.option("--port", default=8080, show_default=True, help="Server port.")
@click.option("--timeout", default=30, show_default=True, help="Connection timeout in seconds.")
@click.option("--token", help="API authentication token.")
@click.option("--username", help="Username for basic auth.")
@click.option("--password", help="Password for basic auth.")
@click.option("--format", type=click.Choice(["json", "table", "csv"]), default="table", show_default=True, help="Output format.")
@click.option("--verbose", is_flag=True, help="Enable verbose logging.")
@click.option("--quiet", is_flag=True, help="Suppress all output except errors.")
def deploy(host, port, timeout, token, username, password, format, verbose, quiet):
"""Deploy the application to a remote server."""
click.echo(f"Deploying to {host}:{port} as {'token auth' if token else username}")
if __name__ == "__main__":
deploy()
Output of python option_groups.py --help:
Usage: deploy [OPTIONS]
Deploy the application to a remote server.
Connection
+-------------+-----------------------------------------------+
| --host | Server hostname. [default: localhost] |
| --port | Server port. [default: 8080] |
| --timeout | Connection timeout in seconds. [default: 30] |
+-------------+-----------------------------------------------+
Authentication
+-------------+-----------------------------------------------+
| --token | API authentication token. |
| --username | Username for basic auth. |
| --password | Password for basic auth. |
+-------------+-----------------------------------------------+
Output
+-------------+-----------------------------------------------+
| --format | Output format. [default: table] |
| --verbose | Enable verbose logging. |
| --quiet | Suppress all output except errors. |
+-------------+-----------------------------------------------+
The key is that the keys in OPTION_GROUPS match the Click command name (the function name by default, or the name= argument in the decorator). Options not listed in any group fall through to a default “Other options” panel at the bottom, so you can migrate groups incrementally without losing any flags from the help page.
Grouping Subcommands in a Group CLI
If your CLI uses a command group (the pattern where mytool db migrate, mytool db rollback, and mytool server start are subcommands), rich-click can group those subcommands into labeled sections too. This uses click.rich_click.COMMAND_GROUPS with the same dictionary pattern.
# command_groups.py
import rich_click as click
click.rich_click.COMMAND_GROUPS = {
"cli": [
{
"name": "Database",
"commands": ["db-init", "db-migrate", "db-reset"],
},
{
"name": "Server",
"commands": ["server-start", "server-stop", "server-status"],
},
]
}
@click.group()
def cli():
"""My Application CLI."""
pass
@cli.command("db-init")
def db_init():
"""Initialize the database schema."""
click.echo("Initializing DB...")
@cli.command("db-migrate")
@click.option("--steps", default=1, show_default=True, help="Number of migrations to apply.")
def db_migrate(steps):
"""Apply pending database migrations."""
click.echo(f"Applying {steps} migration(s)...")
@cli.command("db-reset")
@click.option("--force", is_flag=True, help="Skip confirmation prompt.")
def db_reset(force):
"""Drop and recreate the database."""
if not force:
click.confirm("This will delete all data. Continue?", abort=True)
click.echo("Resetting database...")
@cli.command("server-start")
@click.option("--port", default=8000, show_default=True, help="Port to listen on.")
def server_start(port):
"""Start the application server."""
click.echo(f"Server running on port {port}...")
@cli.command("server-stop")
def server_stop():
"""Gracefully stop the server."""
click.echo("Stopping server...")
@cli.command("server-status")
def server_status():
"""Show the server's current status."""
click.echo("Server status: running")
if __name__ == "__main__":
cli()
Output of python command_groups.py --help:
Usage: cli [OPTIONS] COMMAND [ARGS]...
My Application CLI.
Database
+-------------+-------------------------------------------+
| db-init | Initialize the database schema. |
| db-migrate | Apply pending database migrations. |
| db-reset | Drop and recreate the database. |
+-------------+-------------------------------------------+
Server
+-------------+-------------------------------------------+
| server-start | Start the application server. |
| server-stop | Gracefully stop the server. |
| server-status| Show the server's current status. |
+-------------+-------------------------------------------+
The command group name in COMMAND_GROUPS must match the Click group name. Subcommands listed in no group appear in a default “Commands” section — useful for utility commands like version or help that don’t belong in a domain group.
Theming and Color Configuration
Beyond layout, rich-click exposes a configuration object that controls every color and style element in the help output. You set these as module-level assignments on click.rich_click before any commands are defined. The settings apply globally to all commands in that script.
# theming.py
import rich_click as click
# -- Theming configuration --
click.rich_click.STYLE_OPTION = "bold cyan"
click.rich_click.STYLE_SWITCH = "bold green"
click.rich_click.STYLE_METAVAR = "dim yellow"
click.rich_click.STYLE_HELPTEXT = "white"
click.rich_click.STYLE_HELPTEXT_FIRST_LINE = "bold white"
click.rich_click.STYLE_ERRORS_SUGGESTION = "italic dim"
click.rich_click.STYLE_OPTIONS_TABLE_BOX = "ROUNDED"
click.rich_click.MAX_WIDTH = 100
click.rich_click.SHOW_ARGUMENTS = True
click.rich_click.GROUP_ARGUMENTS_OPTIONS = True
@click.command()
@click.argument("filename")
@click.option("--output", "-o", help="Output file path.")
@click.option("--format", type=click.Choice(["json", "csv", "yaml"]), default="json", show_default=True, help="Output format.")
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
def convert(filename, output, format, dry_run):
"""Convert FILENAME to another format.
Reads the input file and writes a converted version.
Supports JSON, CSV, and YAML output.
"""
click.echo(f"Converting {filename} to {format}...")
if __name__ == "__main__":
convert()
Output:
Usage: theming.py [OPTIONS] FILENAME
Convert FILENAME to another format.
Reads the input file and writes a converted version. Supports JSON, CSV, and YAML output.
+------------+-----------------------------------------+
| Option | Description |
+------------+-----------------------------------------+
| FILENAME | (Required argument) |
| --output | Output file path. |
| --format | Output format. [default: json] |
| --dry-run | Preview changes without writing. |
| --help | Show this message and exit. |
+------------+-----------------------------------------+
The STYLE_* settings accept any style string that rich understands: color names ("cyan", "red"), hex colors ("#ff6b6b"), and style modifiers ("bold", "dim", "italic"). The MAX_WIDTH setting caps the help panel width — useful if your terminal is very wide and you want the output to stay readable. SHOW_ARGUMENTS adds positional arguments to the options table alongside flags, so users see everything in one place.
Epilog Formatting with Markdown
Click supports an epilog parameter on commands — text that appears after the options table. By default, Click strips all formatting from it. With rich-click you can write the epilog as Markdown and have it rendered as rich text, complete with bold, links, and code spans.
# epilog_demo.py
import rich_click as click
click.rich_click.USE_MARKDOWN = True
click.rich_click.USE_MARKDOWN_EMOJI = False # optional: disable emoji parsing
EPILOG = """
**Examples:**
Run a dry-run check:
mytool check --dry-run input.csv
Convert with verbose output:
mytool check --verbose --output result.json input.csv
See the full documentation at https://docs.example.com/mytool
"""
@click.command(epilog=EPILOG)
@click.argument("input_file")
@click.option("--output", "-o", default="output.json", show_default=True, help="Output file.")
@click.option("--dry-run", is_flag=True, help="Preview without writing.")
@click.option("--verbose", "-v", is_flag=True, help="Show detailed progress.")
def check(input_file, output, dry_run, verbose):
"""Validate and convert INPUT_FILE.
Reads a CSV or JSON input file, validates each row,
and writes the cleaned output to the specified path.
"""
click.echo(f"Checking {input_file}...")
if __name__ == "__main__":
check()
Output (epilog section):
Examples:
Run a dry-run check:
mytool check --dry-run input.csv
Convert with verbose output:
mytool check --verbose --output result.json input.csv
See the full documentation at https://docs.example.com/mytool
Setting USE_MARKDOWN = True applies globally — every command’s help text and epilog will be parsed as Markdown. Code blocks in the epilog (fenced with backticks or indented by four spaces) render with syntax highlighting. This is ideal for “Examples” sections, which are one of the most read parts of any CLI help page.
Real-Life Example: A File Conversion CLI
This project ties together option grouping, command grouping, theming, and Markdown epilog into a realistic multi-command CLI tool. It is a data file converter with two subcommands: convert and validate.
# file_converter_cli.py
import rich_click as click
import json
import csv
import os
# --- rich-click configuration ---
click.rich_click.USE_MARKDOWN = True
click.rich_click.MAX_WIDTH = 90
click.rich_click.SHOW_ARGUMENTS = True
click.rich_click.STYLE_OPTION = "bold cyan"
click.rich_click.STYLE_SWITCH = "bold green"
click.rich_click.OPTION_GROUPS = {
"convert": [
{
"name": "Input / Output",
"options": ["--output", "--format"],
},
{
"name": "Behaviour",
"options": ["--dry-run", "--verbose", "--skip-errors"],
},
],
"validate": [
{
"name": "Validation Rules",
"options": ["--required", "--max-rows"],
},
{
"name": "Reporting",
"options": ["--report", "--verbose"],
},
],
}
click.rich_click.COMMAND_GROUPS = {
"fileconv": [
{
"name": "Data Commands",
"commands": ["convert", "validate"],
},
]
}
CONVERT_EPILOG = """
**Examples:**
Convert a JSON file to CSV:
fileconv convert data.json --format csv --output data.csv
Dry-run to preview without writing:
fileconv convert data.json --dry-run --verbose
"""
VALIDATE_EPILOG = """
**Examples:**
Validate a file with required columns:
fileconv validate data.csv --required name --required email
Save a validation report:
fileconv validate data.csv --report report.txt
"""
@click.group()
def fileconv():
"""File Conversion and Validation CLI.
Convert between JSON and CSV formats and validate file structure.
"""
pass
@fileconv.command(epilog=CONVERT_EPILOG)
@click.argument("input_file")
@click.option("--output", "-o", help="Output file path. Defaults to INPUT_FILE with new extension.")
@click.option("--format", "fmt", type=click.Choice(["json", "csv"]), required=True, help="Target format.")
@click.option("--dry-run", is_flag=True, help="Preview conversion without writing output.")
@click.option("--verbose", "-v", is_flag=True, help="Show each row as it is processed.")
@click.option("--skip-errors", is_flag=True, help="Continue on row errors instead of aborting.")
def convert(input_file, output, fmt, dry_run, verbose, skip_errors):
"""Convert INPUT_FILE to another format.
Reads JSON or CSV and writes the converted file.
Detects the input format from the file extension.
"""
if not os.path.exists(input_file):
raise click.BadParameter(f"File not found: {input_file}", param_hint="INPUT_FILE")
base, _ = os.path.splitext(input_file)
output = output or f"{base}.{fmt}"
click.echo(f"Reading: {input_file}")
try:
if input_file.endswith(".json"):
with open(input_file) as f:
rows = json.load(f)
if not isinstance(rows, list):
raise click.ClickException("JSON input must be a list of objects.")
elif input_file.endswith(".csv"):
with open(input_file, newline="") as f:
rows = list(csv.DictReader(f))
else:
raise click.ClickException("Unsupported input format. Use .json or .csv")
except (json.JSONDecodeError, csv.Error) as e:
raise click.ClickException(f"Failed to parse input: {e}")
if verbose:
for i, row in enumerate(rows, 1):
click.echo(f" Row {i}: {row}")
if dry_run:
click.echo(f"[dry-run] Would write {len(rows)} rows to: {output}")
return
if fmt == "csv":
if not rows:
raise click.ClickException("No rows to write.")
with open(output, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
else:
with open(output, "w") as f:
json.dump(rows, f, indent=2)
click.echo(f"Written {len(rows)} rows to: {output}")
@fileconv.command(epilog=VALIDATE_EPILOG)
@click.argument("input_file")
@click.option("--required", multiple=True, help="Column that must be present and non-empty. Repeatable.")
@click.option("--max-rows", type=int, help="Fail if row count exceeds this limit.")
@click.option("--report", help="Write validation report to this file path.")
@click.option("--verbose", "-v", is_flag=True, help="List every issue found.")
def validate(input_file, required, max_rows, report, verbose):
"""Validate INPUT_FILE structure and contents.
Checks for required columns, row count limits, and empty values.
Exits with code 1 if any issues are found.
"""
if not os.path.exists(input_file):
raise click.BadParameter(f"File not found: {input_file}", param_hint="INPUT_FILE")
with open(input_file, newline="") as f:
rows = list(csv.DictReader(f))
issues = []
if max_rows and len(rows) > max_rows:
issues.append(f"Row count {len(rows)} exceeds limit of {max_rows}.")
for col in required:
missing = [i+1 for i, r in enumerate(rows) if not r.get(col, "").strip()]
if missing:
issues.append(f"Column '{col}' is empty on rows: {missing[:5]}{'...' if len(missing) > 5 else ''}")
summary = f"Validated {len(rows)} rows. Issues: {len(issues)}"
click.echo(summary)
if verbose:
for issue in issues:
click.echo(f" - {issue}")
if report:
with open(report, "w") as f:
f.write(summary + "\n")
for issue in issues:
f.write(f"- {issue}\n")
click.echo(f"Report written to: {report}")
if issues:
raise click.ClickException(f"{len(issues)} validation issue(s) found.")
if __name__ == "__main__":
fileconv()
Output of python file_converter_cli.py --help:
Usage: fileconv [OPTIONS] COMMAND [ARGS]...
File Conversion and Validation CLI.
Convert between JSON and CSV formats and validate file structure.
Data Commands
+-----------+------------------------------------------------+
| convert | Convert INPUT_FILE to another format. |
| validate | Validate INPUT_FILE structure and contents. |
+-----------+------------------------------------------------+
Output of python file_converter_cli.py convert --help:
Usage: fileconv convert [OPTIONS] INPUT_FILE
Convert INPUT_FILE to another format. Reads JSON or CSV and writes the converted file.
Input / Output
+-----------+-----------------------------------------------------------+
| INPUT_FILE| (Required argument) |
| --output | Output file path. Defaults to INPUT_FILE with new ext. |
| --format | Target format. [required] |
+-----------+-----------------------------------------------------------+
Behaviour
+---------------+---------------------------------------------------+
| --dry-run | Preview conversion without writing output. |
| --verbose | Show each row as it is processed. |
| --skip-errors | Continue on row errors instead of aborting. |
+---------------+---------------------------------------------------+
Examples:
Convert a JSON file to CSV:
fileconv convert data.json --format csv --output data.csv
This project shows the full rich-click pattern in a real tool. Notice how the two subcommands each have their own OPTION_GROUPS configuration — groups only appear on the command they are defined for, not globally. The epilog examples appear after the options table and use Markdown code blocks. You can extend this tool by adding a third subcommand (merge, split, or schema) and adding its group entry to OPTION_GROUPS.
Frequently Asked Questions
Does rich-click change how my CLI actually runs?
No. rich-click only changes help page rendering. The command routing, argument parsing, option validation, and all callback logic are handled by Click as usual. You can verify this by calling your commands with real arguments — rich-click is not in the execution path at all, only in the --help path. If you encounter a behavioral difference, it is almost certainly a Click version incompatibility, not a rich-click issue.
Can I use rich-click on just some commands in a large project?
Yes. You can import rich_click as click in individual modules while keeping standard click imports elsewhere. Since rich-click is a superset of Click, the decorators are compatible. The configuration dict (OPTION_GROUPS, COMMAND_GROUPS) applies only to commands in the module that sets it — other modules using plain click are unaffected. This makes incremental migration safe in large codebases.
Will the colored output cause problems in CI pipelines or when piped to a file?
rich (and by extension rich-click) automatically detects whether the output stream is a terminal using is_tty checks. When output is redirected to a file or run in a CI environment without a TTY, all ANSI color codes are stripped automatically. The help text is still formatted (tables, panels) but without colors. You can force color off with NO_COLOR=1 environment variable, or force it on with FORCE_COLOR=1 for CI systems that support color (like GitHub Actions with GITHUB_ACTIONS=true).
What happens to options I forget to include in OPTION_GROUPS?
Any option not listed in an explicit group is collected into a default “Options” panel that appears at the bottom of the help output. This means you never accidentally hide an option by forgetting to group it — the worst case is that it appears ungrouped. The --help flag itself always appears in this fallback section unless you explicitly group it. This behavior makes it safe to add new options incrementally without updating OPTION_GROUPS immediately.
Should I use rich-click or Typer for a new project?
Use rich-click if you already have a Click codebase you want to improve, or if you prefer Click’s explicit decorator-based style. Use Typer if you are starting from scratch and want a fully type-annotated API where Click’s decorators are generated from function signatures automatically. Both use Click under the hood and both produce rich-formatted output — Typer uses rich natively, while rich-click layers it on top. Typer is a bigger dependency and a bigger migration; rich-click is a drop-in upgrade.
Conclusion
Python rich-click transforms Click help pages from a functional afterthought into a polished, navigable interface. The core tools covered in this article — the drop-in import swap, OPTION_GROUPS for panel-based option organization, COMMAND_GROUPS for structured command menus, STYLE_* settings for theming, and USE_MARKDOWN for rich epilog formatting — cover the full surface area most CLI tools need. All of this works without touching a single line of command logic.
The real-life project above shows the pattern at scale. Try extending it with a merge subcommand that combines two CSV files, or a schema command that infers a JSON Schema from the input data. Each new command gets its own OPTION_GROUPS entry and its own structured, readable help page.
For the full configuration reference — every STYLE_* variable, the USE_RICH_MARKUP flag, and the HEADER_TEXT / FOOTER_TEXT banner options — see the official rich-click documentation on GitHub and the Click documentation.
Related Articles
Frequently Asked Questions
What is ConfigParser used for in Python?
ConfigParser is a built-in Python module for reading and writing configuration files in INI format. It handles settings organized into sections with key-value pairs, making it easy to store and retrieve application configuration without hardcoding values.
What format does ConfigParser use?
ConfigParser uses the INI file format with sections in square brackets ([section]), followed by key-value pairs using = or : as delimiters. Comments start with # or ;. There is always a [DEFAULT] section for fallback values.
How do I read a config file with ConfigParser?
Create a ConfigParser() instance, call config.read('filename.ini'), then access values with config['section']['key'] or config.get('section', 'key'). Use getint(), getfloat(), or getboolean() for type conversion.
Can ConfigParser handle nested sections?
No, ConfigParser does not support nested sections natively. For nested configuration structures, consider using TOML (tomllib in Python 3.11+), YAML (PyYAML), or JSON configuration files instead.
What is the difference between ConfigParser and JSON for configuration?
ConfigParser uses human-friendly INI format with sections and is ideal for simple settings. JSON supports nested structures and lists but lacks comments. ConfigParser has built-in type conversion methods and a DEFAULT section for fallback values, while JSON requires manual type handling.
Related Articles
- How To Use TOML for Configuration Files
- Managing Environment Variables with dotenv
- How To Use Pathlib for File Paths
Continue Learning Python
Tutorials you might also find useful:
- How To Use Python TOML For Configuration Files Instead of INI
- How To Use Python dynaconf for Configuration Management
- How To Work with ZIP Files in Python
- How To Use Python tempfile for Temporary Files and Directories
- How To Read and Write CSV Files in Python
- How To Read and Write JSON Files in Python
Trackbacks/Pingbacks