jiahao.blog

27 Dec 2025

Unpacking Options in Python

Providing type guarantees for many arguments in Python

2 min read

I have been working on pretty interesting problems as part of Git-Mastery (we are looking for open-source contributors!) and one of the most interesting problems I have encountered recently is having to design a wrapper around the Git CLI.

A Git command (or any CLI in general) can comprise of arguments, options, and flags.

git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
	   [--dry-run] <commit>_ | --fixup [(amend|reword):"><commit>]
	   [-F <file> | -m <msg>] [--reset-author] [--allow-empty]
	   [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
	   [(--trailer <token>[(=|:)<value>])…​] [-S[<keyid>]]
	   [--] [<pathspec>…​]

How do we exactly model so much information within a single function?

git.commit()

Arguments will become parameters:

def commit(
    self,
    pathspec: Optional[str] = None,
) -> None:

But because the order of the flags and options can vary, it does not make sense to expand every single one of them into a separate parameter (because it’s not very sustainable to maintain it in my opinion).

Instead, we represent the flags and options as a TypedDict (docs) instead:

class CommitOptions(TypedDict, total=False):
    all: bool
    reuse_message: str
    message: str
    allow_empty: bool
    no_edit: bool

Then, we can use Unpack (docs) to expand these parameters while providing type hints for the fields of the TypedDict:

def commit(
    self,
    pathspec: Optional[str] = None,
    **options: Unpack[CommitOptions],
) -> None:

It works in tandem with ** to expand these into the respective fields.

When we call this function, we will see that the type hints include every field along with its associated type:

Type hint

Then, to access the fields, you can use options.get() (since we have specified total=False).

The result is a function that we can call with any order of parameters without exploding our function signature at once:

git.commit(
  "dest", 
  message="Add dest/", 
  no_edit=True, 
  allow_empty=True
)

This works especially well when you have a much larger number of flags and options:

class RestoreOptions(TypedDict, total=False):
    source: str

    worktree: bool
    staged: bool

    ours: bool
    theirs: bool

    merge: bool
    conflict: Union[Literal["merge"], Literal["diff3"], Literal["zdiff3"]]

    ignore_unmerged: bool
    ignore_skip_worktree_bits: bool

    recurse_submodules: bool
    no_recurse_submodules: bool

    overlay: bool
    no_overlay: bool

If you are interested in how we have used this pattern in Git-Mastery, feel free to refer to the repo-smith repository: https://github.com/git-mastery/repo-smith

Enjoyed reading?

Consider subscribing to my RSS feed or reaching out to me through email!

You might enjoy...

29 Apr 2025

Building Components for Markdown in Astro

25 Apr 2025

Understanding Systems with Sequence Diagrams

21 Apr 2025

Evolution of Resumes