Package: feud
FeudNot all arguments are bad.Build powerful CLIs with simple idiomatic Python, driven by type hints.About·Features·Installation·Build status·Documentation·Related projects·Contributing·LicensingAbout[!WARNING]Writing command-line interfaces can get messy!Designing agoodCLI can quickly spiral into chaos without the help of
an intuitive CLI framework.Feud builds onClickfor
argument parsing, along withPydanticfor typing, to make CLI building a breeze.FeaturesSimplicityClick is often considered the defacto command-line building utility for Python –
offering far more functionality and better ease-of-use than the standard
library'sargparse.
Despite this, for even the simplest of CLIs, code written using Click can be
somewhat verbose and often requires frequently looking up documentation.Consider the following example command for serving local files on a HTTP server.In red is a typical Click implementation, and in green is the Feud equivalent.Example: Command for running a HTTP web server.# serve.py- import click+ import feud+ from typing import Literal- @click.command- @click.argument("port", type=int, help="Server port.")- @click.option("--watch/--no-watch", type=bool, default=True, help="Watch source code for changes.")- @click.option("--env", type=click.Choice(["dev", "prod"]), default="dev", help="Environment mode.")- def serve(port, watch, env):+ def serve(port: int, *, watch: bool = True, env: Literal["dev", "prod"] = "dev"):-     """Start a local HTTP server."""+     """Start a local HTTP server.++     Parameters+     ----------+     port:+         Server port.+     watch:+         Watch source code for changes.+     env:+         Environment mode.+     """if __name__ == "__main__":-     serve()+     feud.run(serve)Click here to view the generated help screen.Help screen for theservecommand.$pythonserve.py--helpUsage: serve.py [OPTIONS] PORTStart a local HTTP server.╭─ Arguments ────────────────────────────────────────────────────────╮│ *  PORT    INTEGER  [required]                                     │╰────────────────────────────────────────────────────────────────────╯╭─ Options ──────────────────────────────────────────────────────────╮│ --watch/--no-watch                Watch source code for changes.   ││                                   [default: watch]                 ││ --env                 [dev|prod]  Environment mode. [default: dev] ││ --help                            Show this message and exit.      │╰────────────────────────────────────────────────────────────────────╯Click here to see usage examples.python serve.py 8080python serve.py 3000 --watch --env devpython serve.py 4567 --no-watch --env prodThe core design principle behind Feud is to make it as easy as possible
for even beginner Python developers to quickly create sophisticated CLIs.The above function is written in idiomatic Python, adhering to language
standards and using basic core language features such as type hints and
docstrings to declare all of the relevant information about the CLI,
but relying on Feud to carry out the heavy lifting of converting these
language elements into a fully-fledged CLI.Grouping commandsWhile a single command is often all that you need, Feud makes it
straightforward to logically group together related commands into agrouprepresented by a class with commands defined within it.Example: Commands for creating, deleting and listing blog posts.# post.pyimportfeudfromdatetimeimportdateclassPost(feud.Group):"""Manage blog posts."""defcreate(id:int,*,title:str,desc:str|None=None):"""Create a blog post."""defdelete(*ids:int):"""Delete blog posts."""deflist(*,between:tuple[date,date]|None=None):"""View all blog posts, optionally filtering by date range."""if__name__=="__main__":feud.run(Post)Click here to view the generated help screen.Help screen for thepostgroup.$pythonpost.py--helpUsage: post.py [OPTIONS] COMMAND [ARGS]...Manage blog posts.╭─ Options ──────────────────────────────────────────────────────────╮│ --help      Show this message and exit.                            │╰────────────────────────────────────────────────────────────────────╯╭─ Commands ─────────────────────────────────────────────────────────╮│ create   Create a blog post.                                       ││ delete   Delete blog posts.                                        ││ list     View all blog posts, optionally filtering by date range.  │╰────────────────────────────────────────────────────────────────────╯Help screen for thelistcommand within thepostgroup.$pythonpost.pylist--helpUsage: post.py list [OPTIONS]View all blog posts, optionally filtering by date range.╭─ Options ──────────────────────────────────────────────────────────╮│ --between    <DATE DATE>...                                        ││ --help                       Show this message and exit.           │╰────────────────────────────────────────────────────────────────────╯Click here to see usage examples.python post.py create 1 --title "My First Post"python post.py create 2 --title "My First Detailed Post" --desc "Hi!"python post.py delete 1 2python post.py listpython post.py list --between 2020-01-30 2021-01-30Alternatively, if you already have some functions defined that you would like
to run as commands, you can simply provide them tofeud.runand it will
automatically generate and run a group with those commands.# post.pyimportfeudfromdatetimeimportdatedefcreate_post(id:int,*,title:str,desc:str|None=None):"""Create a blog post."""defdelete_posts(*ids:int):"""Delete blog posts."""deflist_posts(*,between:tuple[date,date]|None=None):"""View all blog posts, optionally filtering by date range."""if__name__=="__main__":feud.run([create_post,delete_posts,list_posts])You can also use adictto rename the generated commands:feud.run({"create":create_post,"delete":delete_posts,"list":list_posts})For more complex applications, you can also nest commands in sub-groups:feud.run({"list":list_posts,"modify":[create_post,delete_posts]})If commands are defined in another module, you can also
run the module directly and Feud will pick up all runnable objects:importpostfeud.run(post)You can even callfeud.run()without providing any object, and it will
automatically discover all runnable objects in the current module.As you can see, building a CLI using Feud does not require learning many new
magic methods or a domain-specific language – you can just use the simple
Python you know and ❤️!Registering command sub-groupsGroups can be registered as sub-groups under other groups. This is a common
pattern in CLIs, allowing for interfaces packed with lots of functionality,
but still organized in a sensible way.Example: CLI with the following structure for running and managing a blog.blog: Group to manage and serve a blog.serve: Command to run the blog HTTP server.post: Sub-group to manage blog posts.create: Command to create a blog post.delete: Command to delete blog posts.list: Command to view all blog posts.# blog.pyimportfeudfromdatetimeimportdatefromtypingimportLiteralclassBlog(feud.Group):"""Manage and serve a blog."""defserve(port:int,*,watch:bool=True,env:Literal["dev","prod"]="dev"):"""Start a local HTTP server."""classPost(feud.Group):"""Manage blog posts."""defcreate(id:int,*,title:str,desc:str|None=None):"""Create a blog post."""defdelete(*ids:int):"""Delete blog posts."""deflist(*,between:tuple[date,date]|None=None):"""View all blog posts, optionally filtering by date range."""Blog.register(Post)if__name__=="__main__":feud.run(Blog)Click here to view the generated help screen.Help screen for thebloggroup.$pythonblog.py--helpUsage: blog.py [OPTIONS] COMMAND [ARGS]...Manage and serve a blog.╭─ Options ──────────────────────────────────────────────────────────╮│ --help      Show this message and exit.                            │╰────────────────────────────────────────────────────────────────────╯╭─ Command groups ───────────────────────────────────────────────────╮│ post         Manage blog posts.                                    │╰────────────────────────────────────────────────────────────────────╯╭─ Commands ─────────────────────────────────────────────────────────╮│ serve        Start a local HTTP server.                            │╰────────────────────────────────────────────────────────────────────╯Help screen for theservecommand in thebloggroup.$pythonblog.pyserve--helpUsage: blog.py serve [OPTIONS] PORTStart a local HTTP server.╭─ Arguments ────────────────────────────────────────────────────────╮│ *  PORT    INTEGER  [required]                                     │╰────────────────────────────────────────────────────────────────────╯╭─ Options ──────────────────────────────────────────────────────────╮│ --watch/--no-watch                [default: watch]                 ││ --env                 [dev|prod]  [default: dev]                   ││ --help                            Show this message and exit.      │╰────────────────────────────────────────────────────────────────────╯Help screen for thepostsub-group in thebloggroup.$pythonblog.pypost--helpUsage: blog.py post [OPTIONS] COMMAND [ARGS]...Manage blog posts.╭─ Options ──────────────────────────────────────────────────────────╮│ --help      Show this message and exit.                            │╰────────────────────────────────────────────────────────────────────╯╭─ Commands ─────────────────────────────────────────────────────────╮│ create   Create a blog post.                                       ││ delete   Delete blog posts.                                        ││ list     View all blog posts, optionally filtering by date range.  │╰────────────────────────────────────────────────────────────────────╯Help screen for thelistcommand within thepostsub-group.$pythonblog.pypostlist--helpUsage: blog.py post list [OPTIONS]View all blog posts, optionally filtering by date range.╭─ Options ──────────────────────────────────────────────────────────╮│ --between    <DATE DATE>...                                        ││ --help                       Show this message and exit.           │╰────────────────────────────────────────────────────────────────────╯Click here to see usage examples.python blog.py serve 8080 --no-watch --env prodpython blog.py post create 1 --title "My First Post!"python blog.py post list --between 2020-01-30 2021-01-30Powerful typingFeud is powered byPydantic– a
validation library with extensive support for many data types, including:simple types such as integers and dates,complex types such as emails, IP addresses, file/directory paths, database
connection strings,constrained types (e.g. positive/negative integers or past/future dates).pydantic-extra-typesis
an optional dependency offering additional types such as country names,
payment card numbers, phone numbers, colours, latitude/longitude and more.Custom annotated types with user-defined validation functions can also be
defined with Pydantic.Example: Command for generating audio samples from text prompts using
a machine learning model, and storing produced audio files in an output
directory.At least onetext prompt must be provided.No more than fivetext prompts can be provided.Each text prompt can have amaximum of 12 characters.The model is specified by a path to afile that must exist.The output directory is a path to afolder that must exist.# generate.pyimportfeudfrompydanticimportFilePath,DirectoryPath,conlist,constrdefgenerate(prompts:conlist(constr(max_length=12),min_length=1,max_length=5),*,model:FilePath,output:DirectoryPath,):"""Generates audio from prompts using a trained model."""if__name__=="__main__":feud.run(generate)Click here to view the generated help screen.Help screen for thegeneratecommand.$pythongenerate.py--helpUsage: generate.py [OPTIONS] [PROMPTS]...Generates audio from prompts using a trained model.╭─ Arguments ────────────────────────────────────────────────────────╮│ PROMPTS    TEXT                                                    │╰────────────────────────────────────────────────────────────────────╯╭─ Options ──────────────────────────────────────────────────────────╮│ *  --model     FILE       [required]                               ││ *  --output    DIRECTORY  [required]                               ││    --help                 Show this message and exit.              │╰────────────────────────────────────────────────────────────────────╯Click here to see usage examples.If we run the script without prompts, we get an error that at least one prompt
must be provided.$pythongenerate.py--modelmodels/real_model.pt--outputaudio/Usage: generate.py [OPTIONS] [PROMPTS]...Try 'generate.py --help' for help╭─ Error ──────────────────────────────────────────────────────────────────────╮│ 1 validation error for command 'generate'                                    ││ [PROMPTS]...                                                                 ││   List should have at least 1 item after validation, not 0 [input_value=()]  │╰──────────────────────────────────────────────────────────────────────────────╯If we provide a prompt longer than 12 characters, we also get an error.$pythongenerate.py"dog barking""cat meowing""fish blubbing"--modelmodels/real_model.pt--outputaudio/Usage: generate.py [OPTIONS] [PROMPTS]...Try 'generate.py --help' for help╭─ Error ──────────────────────────────────────────────────────────────────────╮│ 1 validation error for command 'generate'                                    ││ [PROMPTS]... [2]                                                             ││   String should have at most 12 characters [input_value='fish blubbing']     │╰──────────────────────────────────────────────────────────────────────────────╯FilePathindicates that the file must already exist, so we get an error if we
provide a non-existent file.$pythongenerate.py"dog barking""cat meowing"--modelmodels/fake_model.ptUsage: generate.py [OPTIONS] [PROMPTS]...Try 'generate.py --help' for help╭─ Error ──────────────────────────────────────────────────────────────────────╮│ Invalid value for '--model': File 'models/fake_model.pt' does not exist.     │╰──────────────────────────────────────────────────────────────────────────────╯DirectoryPathindicates that the path must be a directory, so we
get an error if we provide a file.$pythongenerate.py"dog barking""cat meowing"--outputaudio.txtUsage: generate.py [OPTIONS] [PROMPTS]...Try 'generate.py --help' for help╭─ Error ──────────────────────────────────────────────────────────────────────╮│ Invalid value for '--output': Directory 'audio.txt' is a file.               │╰──────────────────────────────────────────────────────────────────────────────╯By relying on Pydantic to handle the hard work of validation, we can contain all
of the required CLI constraints in a simple function signature, leaving you to focus
on the important part – implementing your commands.Highly configurable and extensibleWhile designed to be simpler than Click, this comes with the trade-off that
Feud is also more opinionated than Click and only directly implements a subset
of its functionality.However, Feud was designed to allow for Click to seamlessly slot in whenever
manual overrides are necessary.Example: Useclick.password_optionto securely prompt the user for a password, but still validate based on the
type hint (length should be ≥ 10 characters).# login.pyimportfeudfromfeudimportclickfrompydanticimportconstr@click.password_option("--password",help="The user's password (≥ 10 characters).")deflogin(*,username:str,password:constr(min_length=10)):"""Log in as a user.Parameters----------username:The user's username."""if__name__=="__main__":feud.run(login)Click here to view the generated help screen.Help screen for thelogincommand.$pythonlogin.py--helpUsage: login.py [OPTIONS]Log in as a user.╭─ Options ──────────────────────────────────────────────────────────╮│ *  --username    TEXT  The user's username. [required]             ││    --password    TEXT  The user's password (≥ 10 characters).      ││    --help              Show this message and exit.                 │╰────────────────────────────────────────────────────────────────────╯Click here to see usage examples.$pythonlogin.py--usernamealicePassword: ***Repeat for confirmation: ***Usage: login.py [OPTIONS]Try 'login.py --help' for help╭─ Error ────────────────────────────────────────────────────────────╮│ 1 validation error for command 'login'                             ││ --password                                                         ││   String should have at least 10 characters [input_value=hidden]   │╰────────────────────────────────────────────────────────────────────╯InstallationYou can install Feud usingpip.The latest stable version of Feud can be installed with the following command.pip install "feud[all]"This installs Feud with the optional dependencies:rich-click(can install individually withpip install "feud[rich]")Provides improved formatting for CLIs produced by Feud.pydantic-extra-types(can install individually withpip install "feud[extra-types]")Provides additional types that can be used as type hints for Feud commands.email-validator(can install individually withpip install "feud[email]")Provides Pydantic support for email validation.To install Feud without any optional dependencies, simply runpip install feud.[!CAUTION]
Feudwill breakif used with postponed type hint evaluation (PEP563), i.e.:from__future__importannotationsThis is because Feud relies on type hint evaluation in order to determine the expected input type for command parameters.Improved formatting with RichBelow is a comparison of Feud with and withoutrich-click.With Rich-formatted outputWithout Rich-formatted outputBuild statusmasterdevDocumentationAPI reference:
Library documentation for public modules, classes and functions.Related projectsFeud either relies heavily on, or was inspired by the following
packages. It would be greatly appreciated if you also supported the below
maintainers and the work they have done that Feud has built upon.ProjectDescriptionClickby@palletsFeud is essentially a wrapper around Click that takes classes and functions
with type hints and intelligently 'compiles' them into a ready-to-use Click
generated CLI.Rich Clickby@ewelsA shim around Click that renders help output nicely usingRich.Pydanticby@samuelcolvinPydantic is a validation package that makes it easy to declaratively validate
input data based on type hints.The package offers support for common standard library types, plus more complex
types which can also be used as type hints in Feud commands for input validation.Typerby@tiangoloTyper shares a similar ideology to Feud, in that building CLIs should be
simple and not require learning new functions or constantly referring to
library documentation. Typer is also based on Click.Typer is a more complete library for building CLIs overall, but currently
lacks support for more complex types such as those offered by Pydantic.Thorby@railsThough not a Python package, the highly object-oriented design of Thor (a CLI
building package in Ruby) – in particular the use of classes to define command
groups – greatly influenced the implementation of thefeud.Groupclass.ContributingAll contributions to this repository are greatly appreciated. Contribution guidelines can be foundhere.We're living in an imperfect world!Feud is in a public beta-test phase, likely withlotsof bugs. Pleaseleave feedbackif you come across anything strange!LicensingFeud is released under theMITlicense.Feud© 2023-2025, Edwin Onuonga - Released under theMITlicense.Authored and maintained by Edwin Onuonga.