-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
335: Practice Exercise: Building a Config-Driven CLI Tool
I've noticed a recurring pattern when developers start building CLI tools: they confuse "having a settings file" with "building a config-driven tool." It's a subtle distinction, but it's the difference between a tool that requires a code commit every time a requirement changes and a tool that can be repurposed by someone who doesn't even know how to use a debugger.
The Myth: A settings.py File is "Config-Driven"
Many of you likely have a habit of creating a settings.py or a constants.py file where you store a list of API endpoints, database names, or folder paths. You might think, "I've decoupled my configuration from my logic," and in a basic sense, you have. But look at this example of a tool meant to backup specific directories:
# settings.py
BACKUP_PATHS = ["/var/log/nginx", "/home/user/docs", "/etc/ssh"]
BACKUP_DESTINATION = "/mnt/backup_drive"
If your boss comes to you tomorrow and says, "We need to add the /var/www/html folder to the backup list," what happens? You open the editor, change the list, commit the code, and redeploy the tool. The "configuration" is still code. You're still in the deployment loop for a simple data change. That's not a config-driven tool; that's just a tool with global variables.
The Reality: The Engine and the Blueprint
A true config-driven CLI separates the Engine (the Python code that knows how to do things) from the Blueprint (a data file—YAML, JSON, or TOML—that tells the engine what to do). In a config-driven architecture, the Python code shouldn't care what the specific paths are; it should only care how to process any list of paths provided to it via an external file.
I prefer YAML for this because it's human-readable, but JSON works just as well. Imagine the same backup tool, but this time the Python script reads a config.yaml file. Now, a system administrator can add a new directory to the backup list by editing a text file on the server. They don't need Git, they don't need a Python environment, and they certainly don't need to touch your beautiful, tested source code.
Here is how I typically structure the "Engine" side of this: I load the config into a dictionary at startup and then use that dictionary to drive the logic. If I'm using argparse, I can even use the config file to dynamically generate the available choices for a command-line argument.
import yaml
import argparse
# The Engine
def main():
with open("config.yaml", "r") as f:
config = yaml.safe_load(f)
parser = argparse.ArgumentParser()
# We dynamically set the choices based on the config file!
parser.add_argument(
"--target",
choices=config['targets'].keys(),
help="Which target to backup?"
)
args = parser.parse_args()
if args.target:
path = config['targets'][args.target]
print(f"Backing up {args.target} from {path}...")
if __name__ == "__main__":
main()
By doing this, you've shifted the power. The Python script is now a generic processor. If you want to change the targets, you edit the YAML. If you want to change how the backup is performed (e.g., adding compression), you edit the Python. That separation is what makes a tool maintainable in a production environment.
📋 Practical Task
Exercise: Build a Dynamic API Health Checker
Your goal is to build a CLI tool that checks the status of various web services. However, the tool must be entirely config-driven. You cannot hardcode the names or URLs of the services in your Python file.
Requirements:
- Create a file named
services.json. This file should contain a dictionary where the keys are friendly names of services (e.g., "Google", "GitHub") and the values are their corresponding URLs. - Write a Python script that:
- Loads the
services.jsonfile. - Uses
argparseto accept a--serviceargument. - The
--serviceargument must have itschoicesdynamically populated from the keys in the JSON file. - When a service is selected, the tool should use the
requestslibrary (orurllib) to send a GET request to the URL and print whether the service is "UP" (status code 200) or "DOWN".
- Loads the
Test your implementation: Add a new service to your JSON file and run the tool again. Notice how the --help output of your CLI automatically updates to include the new service without you changing a single line of Python code.
There are no comments for now.