Hello! In our last lesson, we established a solid foundation for script monitoring by setting up the logging module. You learned how to create a detailed, timestamped record of your script's operations. However, we hardcoded the configuration, such as the log level and filename, directly into our script. This isn't ideal—if you need to change a setting, you have to edit the source code.
Today, we'll solve this by learning how to load non-secret settings from a configuration file. This practice allows you to separate your script's logic from its configuration, making your automations far more flexible and easier for others (and your future self) to manage. We'll use Python's built-in configparser module to read settings from a standard INI file, a common format for application configuration.
What is a Configuration File?
For many applications, it's useful to store settings in an external file. This could include file paths, server addresses, processing thresholds, or, as we saw in the last lesson, logging preferences. One of the most common formats for this is the INI file (short for "initialization").
INI files are simple text files with a straightforward structure that is easy for both humans and programs to read. They consist of:
- Sections: Declared with square brackets, like
[database]or[logging]. They act like folders to group related settings. - Key-Value Pairs: Each section contains settings written as
key = value. For example,level = INFO. - Comments: Lines starting with a
#or;are ignored and can be used for notes.
The freeCodeCamp article "How to Parse INI Config Files in Python" provides a great visual example.
How to Parse INI Config Files in Python with Configparser
Start by reading the introduction to understand the purpose of config files. Then, focus on the section INI File Format to see a clear example of what an INI file looks like.
This section explains the basic structure with sections and key-value pairs. As you read, notice how settings for database, server, and logging are grouped under their respective section headers.
Let's create our own simple configuration file named config.ini to use in our examples. This file will store settings for logging and for a hypothetical file processing script.
config.ini
# Configuration for our automation script
[logging]
level = INFO
file = automation.log
file_mode = w ; 'w' to overwrite, 'a' to append
[file_processing]
input_directory = C:\Users\YourUser\Documents\InputData
output_directory = C:\Users\YourUser\Documents\OutputData
max_file_size_mb = 100
process_recursively = true
This file neatly organizes our settings, making them easy to find and change without touching any Python code.
Reading Configuration with configparser
Python includes a built-in module, configparser, specifically for working with INI files. Because it's part of the standard library, you don't need to install anything.
The process is straightforward:
- Import the
ConfigParserclass. - Create an instance of the class.
- Use the
.read()method to load your file. - Access values using a dictionary-like syntax:
config['section_name']['key_name'].
This video from Portfolio Courses offers an excellent walkthrough of these fundamental steps.
Read An INI Config File With ConfigParser | Python Tutorial
This video demonstrates the entire process of setting up an INI file and reading it with Python's configparser.
Watch from the beginning until the end of the basic reading example. The first part explains the INI format, and the second part shows how to import configparser, read a file, and access the values. Pay close attention to the config['section']['key'] syntax.
Here's a code snippet that reads our config.ini file and prints a couple of values:
import configparser
from pathlib import Path
# Create a ConfigParser object
config = configparser.ConfigParser()
# Define the path to the config file
CONFIG_FILE = Path("config.ini")
# Read the config file
# .read() returns a list of files it successfully read.
# If the file doesn't exist, it returns an empty list and doesn't raise an error.
if not config.read(CONFIG_FILE):
print(f"Error: Config file not found at {CONFIG_FILE}")
# In a real script, you would likely exit here.
exit()
# Access values
log_file = config['logging']['file']
input_dir = config['file_processing']['input_directory']
print(f"Log file from config: {log_file}")
print(f"Input directory from config: {input_dir}")
Handling Data Types and Providing Fallbacks
There are two critical details to understand when using configparser:
- Everything is a string. When you read
max_file_size_mb = 100, the value you get in Python is the string'100', not the integer100. Similarly,process_recursively = truegives you the string'true', not the booleanTrue. - Missing keys cause errors. If you try to access a key that doesn't exist, like
config['logging']['nonexistent_key'], your script will crash with aKeyError.
To handle this, configparser provides special "getter" methods that both convert the type and provide a safe default value if a key is missing.
config.getint('section', 'key', fallback=default_value)config.getfloat('section', 'key', fallback=default_value)config.getboolean('section', 'key', fallback=default_value)
The getboolean() method is particularly useful because it correctly interprets a variety of case-insensitive values like 'true'/'false', 'yes'/'no', and '1'/'0'.
The official Python documentation explains this clearly.
configparser — Configuration file parser — Python 3.14.2 ...
This official documentation provides the definitive guide on how configparser handles data types and fallbacks.
First, read the section on Supported Datatypes to understand why type conversion is necessary and to see the getint(), getfloat(), and getboolean() methods in action. Then, read the next section on Fallback Values to learn how to provide defaults for missing keys, making your script more robust.
Let's improve our script by using these getter methods.
import configparser
from pathlib import Path
config = configparser.ConfigParser()
CONFIG_FILE = Path("config.ini")
config.read(CONFIG_FILE)
# --- Reading values safely ---
# Get an integer, defaulting to 50 if the key is missing.
max_size = config.getint(
'file_processing',
'max_file_size_mb',
fallback=50
)
# Get a boolean, defaulting to False.
recursive = config.getboolean(
'file_processing',
'process_recursively',
fallback=False
)
print(f"Max file size: {max_size} (Type: {type(max_size)})")
print(f"Process recursively: {recursive} (Type: {type(recursive)})")
Now, even if you remove max_file_size_mb from your config.ini, the script will run without errors, using the fallback value of 50.
Putting It All Together: A Configurable Logging Setup
Let's apply what you've learned to the logging setup from our previous lesson. We'll modify the script to pull its configuration from config.ini, allowing us to change the log level from INFO to DEBUG for troubleshooting without editing the code.
import configparser
import logging
from pathlib import Path
# 1. --- Load Configuration ---
config = configparser.ConfigParser()
config.read('config.ini')
# Get logging settings with sensible fallbacks
log_level_str = config.get('logging', 'level', fallback='INFO').upper()
log_file_path = config.get('logging', 'file', fallback='automation.log')
log_file_mode = config.get('logging', 'file_mode', fallback='w')
# 2. --- Map String to Logging Constant ---
# The logging module needs a constant like logging.INFO, not the string "INFO".
log_levels = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL
}
log_level = log_levels.get(log_level_str, logging.INFO)
# 3. --- Configure Logging ---
logging.basicConfig(
level=log_level,
filename=log_file_path,
filemode=log_file_mode,
encoding="utf-8",
format="%(asctime)s - %(levelname)s - %(message)s"
)
# 4. --- Run Script Logic ---
logging.info("Script started. Logging is configured from config.ini.")
logging.debug("This is a debug message. It will only appear if level is set to DEBUG.")
# Use other settings from the config file
input_dir_str = config.get('file_processing', 'input_directory')
input_dir = Path(input_dir_str)
logging.info(f"Processing files from: {input_dir}")
logging.info("Script finished.")
Now, try it out!
- Run the script with
level = INFOin yourconfig.ini. Thedebugmessage will not appear inautomation.log. - Change the
config.initolevel = DEBUGand run the script again. Thedebugmessage will now be included in the log. You've successfully changed your script's behavior without touching its code!
Conclusion
In this lesson, you've taken a major step toward creating professional, maintainable automation scripts. By separating configuration from code, you make your scripts easier to use, adapt, and troubleshoot.
Here are the key takeaways:
- INI files provide a simple, human-readable format for storing non-secret application settings.
- Python's built-in
configparsermodule is the standard tool for reading and parsing INI files. - All values are read as strings by default. Use methods like
getint(),getfloat(), andgetboolean()for safe type conversion. - Always provide
fallbackvalues for settings to prevent your script from crashing if a key is missing from the configuration file.
We've successfully moved our general settings out of our source code. However, configuration files like config.ini are often checked into version control (like Git), which makes them a terrible place to store sensitive information like passwords, API keys, or server credentials. In our next lesson, we will address this by learning how to load credentials from environment variables, a secure method that keeps your secrets out of your source code.