Python
Completed
-
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
350: Building Subcommands and Shell Completion
If you've been building CLI tools, you've probably hit a wall where a single set of flags isn't enough. You want your tool to feel like git or docker—where you have distinct actions like git push or docker run. In Python, we usually reach for argparse subparsers to do this, but there's a pattern I see almost every junior dev fall into that makes the code a nightmare to maintain.
The If-Else Subcommand Trap
Here is a snippet of a tool I recently reviewed. It's a basic cloud management script. It "works," but look at how the logic is handled:
import argparse
def main():
parser = argparse.ArgumentParser(prog="cloud-tool")
subparsers = parser.add_subparsers(dest="command")
# Deploy command
deploy_parser = subparsers.add_parser("deploy")
deploy_parser.add_argument("--env", required=True)
# Status command
status_parser = subparsers.add_parser("status")
status_parser.add_argument("--service")
args = parser.parse_args()
# The "Trap"
if args.command == "deploy":
print(f"Deploying to {args.env}...")
# ... 50 lines of deployment logic ...
elif args.command == "status":
print(f"Checking status of {args.service}...")
# ... 50 lines of status logic ...
else:
parser.print_help()
if __name__ == "__main__":
main()
The problem here isn't that it doesn't run; it's that the main() function is destined to become a thousand-line monster. Every time you add a new subcommand, your if/elif chain grows. You're mixing the parsing of the arguments with the execution of the logic. It's brittle and hard to test.
Mapping Subcommands to Functions
The professional way to handle this in argparse is using set_defaults. Instead of checking the command name with a string, we tell the parser exactly which function should handle that specific subcommand. This decouples the CLI definition from the business logic.
import argparse
def handle_deploy(args):
print(f"Deploying to {args.env}...")
def handle_status(args):
print(f"Checking status of {args.service}...")
def main():
parser = argparse.ArgumentParser(prog="cloud-tool")
subparsers = parser.add_subparsers(dest="command", required=True)
deploy_parser = subparsers.add_parser("deploy")
deploy_parser.add_argument("--env", required=True)
deploy_parser.set_defaults(func=handle_deploy) # This is the secret sauce
status_parser = subparsers.add_parser("status")
status_parser.add_argument("--service")
status_parser.set_defaults(func=handle_status)
args = parser.parse_args()
# No more if/elif chains. Just call the function mapped to the command.
args.func(args)
if __name__ == "__main__":
main()
Now, main() doesn't care if you have two commands or two hundred. It just finds the function associated with the user's choice and executes it. I highly recommend moving those handle_ functions into their own modules as your project grows.
Making the Shell Actually Help You
Now, let's talk about the "polish." Have you ever used a CLI tool and felt annoyed that you had to keep checking --help because Tab-completion didn't work? If you're distributing a tool for other engineers, shell completion isn't a "nice-to-have"—it's a requirement.
Python's argparse doesn't do this natively because shell completion is a feature of the shell (Bash, Zsh, Fish), not the language. However, the argcomplete library is the industry standard for bridging this gap. It allows your Python script to "tell" the shell what the valid completions are in real-time.
First, you'll need pip install argcomplete. Then, you add one single line to the very top of your script:
import argparse
import argcomplete # Import it here
def handle_deploy(args):
# ... logic ...
pass
def main():
parser = argparse.ArgumentParser(prog="cloud-tool")
# ... add subparsers ...
# This line enables the magic
argcomplete.autocomplete(parser)
args = parser.parse_args()
args.func(args)
The Registration Gotcha
Here is where most people get stuck: they add the code, hit Tab, and... nothing happens. That's because the shell needs to be told to look for argcomplete's hooks. You can't just change the Python code; you have to register the script with your shell.
For a global installation, you can run activate-global-python-argcomplete. But for a specific project, the most reliable way is to run this in your terminal (for Bash):
eval "$(register-python-argcomplete cloud-tool)"
I usually put this in a .bashrc or a setup script. Once registered, your shell will actually execute a "hidden" pass of your Python script whenever the user hits Tab to determine which subcommands and flags are valid based on the current state of the parser.
📋 Practical Task
Build a Task Management CLI with Subcommands and Completion
Your goal is to build a CLI tool called taskmaster that manages a simple todo list. You need to implement the following requirements using the set_defaults(func=...) pattern and argcomplete integration.
- Subcommand:
add- Must take a positional argument
task_name. - Must take an optional flag
--priority(defaulting to "medium").
- Must take a positional argument
- Subcommand:
list- Must take an optional flag
--completed(a boolean/store_true) to filter the list.
- Must take an optional flag
- Subcommand:
remove- Must take a positional argument
task_id(integer).
- Must take a positional argument
Technical Constraints:
- Do NOT use a chain of
if/elifstatements to determine which command to run. Use the function mapping pattern. - Integrate
argcompleteso that the subcommands (add,list,remove) can be tab-completed. - The "business logic" functions don't need to actually save to a database; simply printing
"Adding task [name] with priority [priority]"is sufficient for this exercise.
Verification: To test the completion, remember to run eval "$(register-python-argcomplete taskmaster)" in your terminal before attempting to use Tab-completion.
There are no comments for now.