[{"data":1,"prerenderedAt":2603},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002F":3,"content-directory":2057},{"id":4,"title":5,"body":6,"date":2043,"description":2044,"difficulty":2045,"draft":2046,"extension":2047,"meta":2048,"navigation":251,"path":2049,"seo":2050,"stem":2051,"tags":2052,"updated":2043,"__hash__":2056},"content\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Findex.md","Linting and Type-Checking Python CLI Code",{"type":7,"value":8,"toc":2020},"minimark",[9,34,48,52,57,120,124,127,130,165,169,175,446,465,469,482,735,788,791,881,887,891,915,918,924,1075,1091,1095,1098,1133,1140,1144,1147,1150,1206,1209,1213,1216,1366,1420,1659,1672,1676,1679,1723,1727,1730,1764,1808,1818,1822,1825,1858,1862,1900,1904,1909,1912,1916,1926,1930,1941,1945,1952,1956,1970,1974,1980,1984,2016],[10,11,12,13,17,18,21,22,25,26,29,30,33],"p",{},"Command-line tools accumulate a particular kind of bug. A ",[14,15,16],"code",{},"subprocess.run(f\"...\", shell=True)"," that works until a filename contains a space. An ",[14,19,20],{},"open(path)"," without an encoding that works on every Linux machine and breaks on the first Windows laptop. An option declared as ",[14,23,24],{},"int"," and later used as a string. A ",[14,27,28],{},"print()"," in a library module that corrupts the JSON output of a command three layers up. A core module that quietly imported ",[14,31,32],{},"rich"," and now adds 80 ms to every invocation. None of these needs a test to find — static analysis catches all of them in seconds, before anything runs.",[10,35,36,37,42,43,47],{},"This topic covers the static checks that pay off most for a Python CLI: Ruff for linting and formatting, a type checker that understands Click and Typer, and import contracts that keep the layers of the codebase separate. It sits alongside ",[38,39,41],"a",{"href":40},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002F","pre-commit hooks for CLI projects",", which run these checks on every commit, and ",[38,44,46],{"href":45},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002F","CI\u002FCD pipelines for Python CLIs",", which run them on every push.",[49,50],"inline-diagram",{"name":51},"lint-topic-map",[53,54,56],"h2",{"id":55},"tldr","TL;DR",[58,59,60,72,94,108,114],"ul",{},[61,62,63,67,68,71],"li",{},[64,65,66],"strong",{},"Use Ruff for both linting and formatting."," One fast tool, configured in ",[14,69,70],{},"pyproject.toml",", replaces flake8, isort, pyupgrade, black and several plugins.",[61,73,74,77,78,81,82,85,86,89,90,93],{},[64,75,76],{},"Enable rule sets that matter for CLIs",": security (",[14,79,80],{},"S","), pathlib (",[14,83,84],{},"PTH","), bugbear (",[14,87,88],{},"B","), print statements (",[14,91,92],{},"T20",") — not just the defaults.",[61,95,96,99,100,103,104,107],{},[64,97,98],{},"Type-check with mypy or pyright",", annotate command parameters, and turn ",[14,101,102],{},"ctx.obj"," into a typed object instead of ",[14,105,106],{},"Any",".",[61,109,110,113],{},[64,111,112],{},"Enforce layers with import-linter"," so core logic never imports the CLI framework, terminal libraries or HTTP clients.",[61,115,116,119],{},[64,117,118],{},"Run the same configuration everywhere",": editor, pre-commit and CI.",[53,121,123],{"id":122},"why-static-checks-suit-clis-so-well","Why static checks suit CLIs so well",[10,125,126],{},"A CLI's surface area is broad and shallow: many commands, many options, many interactions with the operating system, each exercised by a handful of tests at most. Tests catch the behaviour you thought to check; static analysis checks every line, including the error path for a Windows user with a non-ASCII username that no test will ever take.",[49,128],{"name":129},"lint-bug-classes",[10,131,132,133,136,137,141,142,145,146,149,150,153,154,156,157,160,161,164],{},"The rules in that table are not style preferences. ",[14,134,135],{},"S602"," flags the shell-injection pattern covered in ",[38,138,140],{"href":139},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis\u002F","avoiding shell injection in Python CLIs",". ",[14,143,144],{},"PLW1514"," flags text-mode ",[14,147,148],{},"open()"," without an encoding, the root of many Windows-only failures. ",[14,151,152],{},"T201"," flags ",[14,155,28],{}," calls, which in a CLI should appear only in the command layer where output is deliberately produced — anywhere else they risk writing to a stdout that a script is parsing. The type checker catches the ",[14,158,159],{},"None"," that reaches a function expecting a ",[14,162,163],{},"Path"," only when a user omits an optional flag.",[53,166,168],{"id":167},"ruff-one-tool-for-lint-and-format","Ruff: one tool for lint and format",[10,170,171,172,174],{},"Ruff implements hundreds of rules from flake8 and its plugins, plus isort, pyupgrade and a Black-compatible formatter, in a single Rust binary that checks a typical CLI codebase in well under a second. Configuration lives in ",[14,173,70],{},":",[176,177,182],"pre",{"className":178,"code":179,"language":180,"meta":181,"style":181},"language-toml shiki shiki-themes github-light github-dark","[tool.ruff]\nline-length = 100\ntarget-version = \"py310\"          # match requires-python\nsrc = [\"src\", \"tests\"]\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"W\", \"I\", \"B\", \"UP\", \"S\", \"PTH\", \"T20\", \"SIM\", \"RUF\"]\nignore = [\"E501\"]                 # the formatter owns line length\n\n[tool.ruff.lint.per-file-ignores]\n\"tests\u002F**\" = [\"S101\", \"S603\", \"S607\"]     # assert and subprocess are normal in tests\n\"src\u002Fmytool\u002Fcli.py\" = [\"T201\"]             # the command layer may print\n\n[tool.ruff.format]\ndocstring-code-format = true\n","toml","",[14,183,184,205,215,229,246,253,271,332,347,352,374,399,414,419,437],{"__ignoreMap":181},[185,186,189,193,197,199,202],"span",{"class":187,"line":188},"line",1,[185,190,192],{"class":191},"sVt8B","[",[185,194,196],{"class":195},"sScJk","tool",[185,198,107],{"class":191},[185,200,201],{"class":195},"ruff",[185,203,204],{"class":191},"]\n",[185,206,208,211],{"class":187,"line":207},2,[185,209,210],{"class":191},"line-length = ",[185,212,214],{"class":213},"sj4cs","100\n",[185,216,218,221,225],{"class":187,"line":217},3,[185,219,220],{"class":191},"target-version = ",[185,222,224],{"class":223},"sZZnC","\"py310\"",[185,226,228],{"class":227},"sJ8bj","          # match requires-python\n",[185,230,232,235,238,241,244],{"class":187,"line":231},4,[185,233,234],{"class":191},"src = [",[185,236,237],{"class":223},"\"src\"",[185,239,240],{"class":191},", ",[185,242,243],{"class":223},"\"tests\"",[185,245,204],{"class":191},[185,247,249],{"class":187,"line":248},5,[185,250,252],{"emptyLinePlaceholder":251},true,"\n",[185,254,256,258,260,262,264,266,269],{"class":187,"line":255},6,[185,257,192],{"class":191},[185,259,196],{"class":195},[185,261,107],{"class":191},[185,263,201],{"class":195},[185,265,107],{"class":191},[185,267,268],{"class":195},"lint",[185,270,204],{"class":191},[185,272,274,277,280,282,285,287,290,292,295,297,300,302,305,307,310,312,315,317,320,322,325,327,330],{"class":187,"line":273},7,[185,275,276],{"class":191},"select = [",[185,278,279],{"class":223},"\"E\"",[185,281,240],{"class":191},[185,283,284],{"class":223},"\"F\"",[185,286,240],{"class":191},[185,288,289],{"class":223},"\"W\"",[185,291,240],{"class":191},[185,293,294],{"class":223},"\"I\"",[185,296,240],{"class":191},[185,298,299],{"class":223},"\"B\"",[185,301,240],{"class":191},[185,303,304],{"class":223},"\"UP\"",[185,306,240],{"class":191},[185,308,309],{"class":223},"\"S\"",[185,311,240],{"class":191},[185,313,314],{"class":223},"\"PTH\"",[185,316,240],{"class":191},[185,318,319],{"class":223},"\"T20\"",[185,321,240],{"class":191},[185,323,324],{"class":223},"\"SIM\"",[185,326,240],{"class":191},[185,328,329],{"class":223},"\"RUF\"",[185,331,204],{"class":191},[185,333,335,338,341,344],{"class":187,"line":334},8,[185,336,337],{"class":191},"ignore = [",[185,339,340],{"class":223},"\"E501\"",[185,342,343],{"class":191},"]                 ",[185,345,346],{"class":227},"# the formatter owns line length\n",[185,348,350],{"class":187,"line":349},9,[185,351,252],{"emptyLinePlaceholder":251},[185,353,355,357,359,361,363,365,367,369,372],{"class":187,"line":354},10,[185,356,192],{"class":191},[185,358,196],{"class":195},[185,360,107],{"class":191},[185,362,201],{"class":195},[185,364,107],{"class":191},[185,366,268],{"class":195},[185,368,107],{"class":191},[185,370,371],{"class":195},"per-file-ignores",[185,373,204],{"class":191},[185,375,377,380,383,385,388,390,393,396],{"class":187,"line":376},11,[185,378,379],{"class":191},"\"tests\u002F**\" = [",[185,381,382],{"class":223},"\"S101\"",[185,384,240],{"class":191},[185,386,387],{"class":223},"\"S603\"",[185,389,240],{"class":191},[185,391,392],{"class":223},"\"S607\"",[185,394,395],{"class":191},"]     ",[185,397,398],{"class":227},"# assert and subprocess are normal in tests\n",[185,400,402,405,408,411],{"class":187,"line":401},12,[185,403,404],{"class":191},"\"src\u002Fmytool\u002Fcli.py\" = [",[185,406,407],{"class":223},"\"T201\"",[185,409,410],{"class":191},"]             ",[185,412,413],{"class":227},"# the command layer may print\n",[185,415,417],{"class":187,"line":416},13,[185,418,252],{"emptyLinePlaceholder":251},[185,420,422,424,426,428,430,432,435],{"class":187,"line":421},14,[185,423,192],{"class":191},[185,425,196],{"class":195},[185,427,107],{"class":191},[185,429,201],{"class":195},[185,431,107],{"class":191},[185,433,434],{"class":195},"format",[185,436,204],{"class":191},[185,438,440,443],{"class":187,"line":439},15,[185,441,442],{"class":191},"docstring-code-format = ",[185,444,445],{"class":213},"true\n",[10,447,448,451,452,455,456,459,460,464],{},[14,449,450],{},"target-version"," matters more than it looks: pyupgrade rules (",[14,453,454],{},"UP",") will suggest syntax only available in that version and newer, so it must match your oldest supported Python or Ruff will \"modernise\" code into something your users cannot run. The per-file ignores encode architecture — tests may use ",[14,457,458],{},"assert"," and subprocesses freely, and only the CLI layer may print. ",[38,461,463],{"href":462},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project\u002F","Configuring Ruff for a CLI project"," goes through the rule sets in detail, including how to adopt them on an existing codebase without a thousand-line diff.",[53,466,468],{"id":467},"types-across-the-cli-boundary","Types across the CLI boundary",[10,470,471,472,475,476,478,479,481],{},"Typer builds its whole interface from type annotations, which makes a Typer CLI unusually friendly to type checkers: the same annotation that tells Typer to convert ",[14,473,474],{},"--retries"," to an ",[14,477,24],{}," tells mypy that the function receives an ",[14,480,24],{},". Click is less direct — decorators add parameters at runtime that the type checker cannot see — but annotating the function parameters yourself gives the same result.",[176,483,487],{"className":484,"code":485,"language":486,"meta":181,"style":181},"language-python shiki shiki-themes github-light github-dark","from pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\napp = typer.Typer()\n\n\ndef render(source: Path, dest: Path) -> int:\n    ...\n    return 0\n\n\n@app.command()\ndef build(\n    source: Annotated[Path, typer.Argument(exists=True, file_okay=False)],\n    out: Annotated[Path | None, typer.Option(\"--out\", \"-o\")] = None,\n) -> None:\n    written = render(source, out or source.with_name(\"site\"))\n    typer.echo(f\"wrote {written} files\", err=True)\n","python",[14,488,489,504,516,520,527,531,542,546,550,566,571,579,583,587,595,605,633,666,676,699],{"__ignoreMap":181},[185,490,491,495,498,501],{"class":187,"line":188},[185,492,494],{"class":493},"szBVR","from",[185,496,497],{"class":191}," pathlib ",[185,499,500],{"class":493},"import",[185,502,503],{"class":191}," Path\n",[185,505,506,508,511,513],{"class":187,"line":207},[185,507,494],{"class":493},[185,509,510],{"class":191}," typing ",[185,512,500],{"class":493},[185,514,515],{"class":191}," Annotated\n",[185,517,518],{"class":187,"line":217},[185,519,252],{"emptyLinePlaceholder":251},[185,521,522,524],{"class":187,"line":231},[185,523,500],{"class":493},[185,525,526],{"class":191}," typer\n",[185,528,529],{"class":187,"line":248},[185,530,252],{"emptyLinePlaceholder":251},[185,532,533,536,539],{"class":187,"line":255},[185,534,535],{"class":191},"app ",[185,537,538],{"class":493},"=",[185,540,541],{"class":191}," typer.Typer()\n",[185,543,544],{"class":187,"line":273},[185,545,252],{"emptyLinePlaceholder":251},[185,547,548],{"class":187,"line":334},[185,549,252],{"emptyLinePlaceholder":251},[185,551,552,555,558,561,563],{"class":187,"line":349},[185,553,554],{"class":493},"def",[185,556,557],{"class":195}," render",[185,559,560],{"class":191},"(source: Path, dest: Path) -> ",[185,562,24],{"class":213},[185,564,565],{"class":191},":\n",[185,567,568],{"class":187,"line":354},[185,569,570],{"class":213},"    ...\n",[185,572,573,576],{"class":187,"line":376},[185,574,575],{"class":493},"    return",[185,577,578],{"class":213}," 0\n",[185,580,581],{"class":187,"line":401},[185,582,252],{"emptyLinePlaceholder":251},[185,584,585],{"class":187,"line":416},[185,586,252],{"emptyLinePlaceholder":251},[185,588,589,592],{"class":187,"line":421},[185,590,591],{"class":195},"@app.command",[185,593,594],{"class":191},"()\n",[185,596,597,599,602],{"class":187,"line":439},[185,598,554],{"class":493},[185,600,601],{"class":195}," build",[185,603,604],{"class":191},"(\n",[185,606,608,611,615,617,620,622,625,627,630],{"class":187,"line":607},16,[185,609,610],{"class":191},"    source: Annotated[Path, typer.Argument(",[185,612,614],{"class":613},"s4XuR","exists",[185,616,538],{"class":493},[185,618,619],{"class":213},"True",[185,621,240],{"class":191},[185,623,624],{"class":613},"file_okay",[185,626,538],{"class":493},[185,628,629],{"class":213},"False",[185,631,632],{"class":191},")],\n",[185,634,636,639,642,645,648,651,653,656,659,661,663],{"class":187,"line":635},17,[185,637,638],{"class":191},"    out: Annotated[Path ",[185,640,641],{"class":493},"|",[185,643,644],{"class":213}," None",[185,646,647],{"class":191},", typer.Option(",[185,649,650],{"class":223},"\"--out\"",[185,652,240],{"class":191},[185,654,655],{"class":223},"\"-o\"",[185,657,658],{"class":191},")] ",[185,660,538],{"class":493},[185,662,644],{"class":213},[185,664,665],{"class":191},",\n",[185,667,669,672,674],{"class":187,"line":668},18,[185,670,671],{"class":191},") -> ",[185,673,159],{"class":213},[185,675,565],{"class":191},[185,677,679,682,684,687,690,693,696],{"class":187,"line":678},19,[185,680,681],{"class":191},"    written ",[185,683,538],{"class":493},[185,685,686],{"class":191}," render(source, out ",[185,688,689],{"class":493},"or",[185,691,692],{"class":191}," source.with_name(",[185,694,695],{"class":223},"\"site\"",[185,697,698],{"class":191},"))\n",[185,700,702,705,708,711,714,717,720,723,725,728,730,732],{"class":187,"line":701},20,[185,703,704],{"class":191},"    typer.echo(",[185,706,707],{"class":493},"f",[185,709,710],{"class":223},"\"wrote ",[185,712,713],{"class":213},"{",[185,715,716],{"class":191},"written",[185,718,719],{"class":213},"}",[185,721,722],{"class":223}," files\"",[185,724,240],{"class":191},[185,726,727],{"class":613},"err",[185,729,538],{"class":493},[185,731,619],{"class":213},[185,733,734],{"class":191},")\n",[10,736,737,738,741,742,745,746,749,750,753,754,756,757,760,761,763,764,766,767,770,771,774,775,779,780,784,785,787],{},"The ",[14,739,740],{},"Annotated"," form keeps the default value where Python and type checkers expect it, instead of hiding it inside ",[14,743,744],{},"typer.Option(...)",", so mypy knows ",[14,747,748],{},"out"," is ",[14,751,752],{},"Path | None"," and forces you to handle the ",[14,755,159],{}," case — here with a fallback — rather than letting it reach ",[14,758,759],{},"render"," and crash. The gaps that remain are predictable: ",[14,762,102],{}," is typed ",[14,765,106],{},", Click's ",[14,768,769],{},"Choice"," produces a plain ",[14,772,773],{},"str",", and some decorators erase signatures. ",[38,776,778],{"href":777},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy\u002F","Type-checking Click and Typer code with mypy"," closes each one, and ",[38,781,783],{"href":782},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer\u002F","using Annotated options in Typer"," covers the ",[14,786,740],{}," style itself.",[10,789,790],{},"A minimal mypy configuration to start from:",[176,792,794],{"className":178,"code":793,"language":180,"meta":181,"style":181},"[tool.mypy]\npython_version = \"3.10\"\nfiles = [\"src\"]\nstrict = true\nwarn_unreachable = true\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\n",[14,795,796,809,817,826,833,840,844,863,873],{"__ignoreMap":181},[185,797,798,800,802,804,807],{"class":187,"line":188},[185,799,192],{"class":191},[185,801,196],{"class":195},[185,803,107],{"class":191},[185,805,806],{"class":195},"mypy",[185,808,204],{"class":191},[185,810,811,814],{"class":187,"line":207},[185,812,813],{"class":191},"python_version = ",[185,815,816],{"class":223},"\"3.10\"\n",[185,818,819,822,824],{"class":187,"line":217},[185,820,821],{"class":191},"files = [",[185,823,237],{"class":223},[185,825,204],{"class":191},[185,827,828,831],{"class":187,"line":231},[185,829,830],{"class":191},"strict = ",[185,832,445],{"class":213},[185,834,835,838],{"class":187,"line":248},[185,836,837],{"class":191},"warn_unreachable = ",[185,839,445],{"class":213},[185,841,842],{"class":187,"line":255},[185,843,252],{"emptyLinePlaceholder":251},[185,845,846,849,851,853,855,857,860],{"class":187,"line":273},[185,847,848],{"class":191},"[[",[185,850,196],{"class":195},[185,852,107],{"class":191},[185,854,806],{"class":195},[185,856,107],{"class":191},[185,858,859],{"class":195},"overrides",[185,861,862],{"class":191},"]]\n",[185,864,865,868,871],{"class":187,"line":334},[185,866,867],{"class":191},"module = [",[185,869,870],{"class":223},"\"tests.*\"",[185,872,204],{"class":191},[185,874,875,878],{"class":187,"line":349},[185,876,877],{"class":191},"disallow_untyped_defs = ",[185,879,880],{"class":213},"false\n",[10,882,883,886],{},[14,884,885],{},"strict = true"," on new code is realistic for a CLI, because most of the code is your own and the main dependencies — Click, Typer, Rich, httpx — ship type information.",[53,888,890],{"id":889},"keeping-the-layers-apart","Keeping the layers apart",[10,892,893,894,898,899,902,903,906,907,910,911,914],{},"Well-structured CLIs keep a thin command layer on top of core logic that knows nothing about the command line, as described in ",[38,895,897],{"href":896},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project\u002F","how to structure a large Python CLI project",". That separation is what makes core logic reusable from other programs, testable without ",[14,900,901],{},"CliRunner",", and fast to import. It is also easy to erode one convenient import at a time: a helper in ",[14,904,905],{},"core\u002F"," that uses ",[14,908,909],{},"rich.print"," for a debug message, a model that imports ",[14,912,913],{},"typer.BadParameter"," to raise a nicer error.",[49,916],{"name":917},"lint-layers-stack",[10,919,920,923],{},[14,921,922],{},"import-linter"," turns the architecture into a checked contract:",[176,925,927],{"className":178,"code":926,"language":180,"meta":181,"style":181},"[tool.importlinter]\nroot_package = \"mytool\"\n\n[[tool.importlinter.contracts]]\nname = \"CLI layers\"\ntype = \"layers\"\nlayers = [\"mytool.cli\", \"mytool.services\", \"mytool.core\"]\n\n[[tool.importlinter.contracts]]\nname = \"Core has no framework dependencies\"\ntype = \"forbidden\"\nsource_modules = [\"mytool.core\"]\nforbidden_modules = [\"typer\", \"click\", \"rich\", \"httpx\"]\n",[14,928,929,942,950,954,971,979,987,1007,1011,1027,1034,1041,1050],{"__ignoreMap":181},[185,930,931,933,935,937,940],{"class":187,"line":188},[185,932,192],{"class":191},[185,934,196],{"class":195},[185,936,107],{"class":191},[185,938,939],{"class":195},"importlinter",[185,941,204],{"class":191},[185,943,944,947],{"class":187,"line":207},[185,945,946],{"class":191},"root_package = ",[185,948,949],{"class":223},"\"mytool\"\n",[185,951,952],{"class":187,"line":217},[185,953,252],{"emptyLinePlaceholder":251},[185,955,956,958,960,962,964,966,969],{"class":187,"line":231},[185,957,848],{"class":191},[185,959,196],{"class":195},[185,961,107],{"class":191},[185,963,939],{"class":195},[185,965,107],{"class":191},[185,967,968],{"class":195},"contracts",[185,970,862],{"class":191},[185,972,973,976],{"class":187,"line":248},[185,974,975],{"class":191},"name = ",[185,977,978],{"class":223},"\"CLI layers\"\n",[185,980,981,984],{"class":187,"line":255},[185,982,983],{"class":191},"type = ",[185,985,986],{"class":223},"\"layers\"\n",[185,988,989,992,995,997,1000,1002,1005],{"class":187,"line":273},[185,990,991],{"class":191},"layers = [",[185,993,994],{"class":223},"\"mytool.cli\"",[185,996,240],{"class":191},[185,998,999],{"class":223},"\"mytool.services\"",[185,1001,240],{"class":191},[185,1003,1004],{"class":223},"\"mytool.core\"",[185,1006,204],{"class":191},[185,1008,1009],{"class":187,"line":334},[185,1010,252],{"emptyLinePlaceholder":251},[185,1012,1013,1015,1017,1019,1021,1023,1025],{"class":187,"line":349},[185,1014,848],{"class":191},[185,1016,196],{"class":195},[185,1018,107],{"class":191},[185,1020,939],{"class":195},[185,1022,107],{"class":191},[185,1024,968],{"class":195},[185,1026,862],{"class":191},[185,1028,1029,1031],{"class":187,"line":354},[185,1030,975],{"class":191},[185,1032,1033],{"class":223},"\"Core has no framework dependencies\"\n",[185,1035,1036,1038],{"class":187,"line":376},[185,1037,983],{"class":191},[185,1039,1040],{"class":223},"\"forbidden\"\n",[185,1042,1043,1046,1048],{"class":187,"line":401},[185,1044,1045],{"class":191},"source_modules = [",[185,1047,1004],{"class":223},[185,1049,204],{"class":191},[185,1051,1052,1055,1058,1060,1063,1065,1068,1070,1073],{"class":187,"line":416},[185,1053,1054],{"class":191},"forbidden_modules = [",[185,1056,1057],{"class":223},"\"typer\"",[185,1059,240],{"class":191},[185,1061,1062],{"class":223},"\"click\"",[185,1064,240],{"class":191},[185,1066,1067],{"class":223},"\"rich\"",[185,1069,240],{"class":191},[185,1071,1072],{"class":223},"\"httpx\"",[185,1074,204],{"class":191},[10,1076,1077,1080,1081,1085,1086,1090],{},[14,1078,1079],{},"lint-imports"," then fails whenever a lower layer imports a higher one, or core imports a framework, and reports the full chain of imports that caused it — including indirect ones. The forbidden contract doubles as a ",[38,1082,1084],{"href":1083},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","startup-time"," guard: heavy libraries cannot creep into modules every command imports. ",[38,1087,1089],{"href":1088},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase\u002F","Enforcing import boundaries in a CLI codebase"," covers contracts for plugins and independent subcommands as well.",[53,1092,1094],{"id":1093},"where-the-checks-run","Where the checks run",[10,1096,1097],{},"The same configuration should run in three places, so the answer never depends on where you ask:",[58,1099,1100,1106,1124],{},[61,1101,1102,1105],{},[64,1103,1104],{},"In the editor",", through the Ruff language server and your editor's mypy or Pylance\u002Fpyright integration. Problems appear as you type, which is when they are cheapest to fix.",[61,1107,1108,1111,1112,1115,1116,1119,1120,107],{},[64,1109,1110],{},"On commit",", through pre-commit: ",[14,1113,1114],{},"ruff check --fix"," and ",[14,1117,1118],{},"ruff format"," on changed files, fast enough that nobody disables them. See ",[38,1121,1123],{"href":1122},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos\u002F","setting up pre-commit for Python CLI repos",[61,1125,1126,1129,1130,1132],{},[64,1127,1128],{},"In CI",", on the whole repository, including slower checks such as mypy over everything and ",[14,1131,1079],{},". This is the gate; the other two are conveniences.",[10,1134,1135,1136,1139],{},"Run all three from the versions pinned in your dev dependency group (",[14,1137,1138],{},"uv add --dev ruff mypy import-linter","), so a new Ruff release with new default behaviour arrives as a reviewed lockfile change rather than a surprise failure on Monday morning.",[53,1141,1143],{"id":1142},"adopting-checks-on-an-existing-codebase","Adopting checks on an existing codebase",[10,1145,1146],{},"Turning on strict checks across a codebase that grew without them produces hundreds of findings, and the temptation is to either fix everything in one heroic pull request or give up. A staged approach works better:",[49,1148],{"name":1149},"lint-adoption-timeline",[1151,1152,1153,1167,1173,1183,1197],"ol",{},[61,1154,1155,1158,1159,1162,1163,1166],{},[64,1156,1157],{},"Format everything"," in one mechanical commit and add its hash to ",[14,1160,1161],{},".git-blame-ignore-revs"," so ",[14,1164,1165],{},"git blame"," skips it.",[61,1168,1169,1172],{},[64,1170,1171],{},"Enable Ruff's defaults",", fix what is quick, and suppress the rest with per-file ignores that you then burn down.",[61,1174,1175,1178,1179,1182],{},[64,1176,1177],{},"Add mypy with lenient settings"," and enable strictness module by module through ",[14,1180,1181],{},"[[tool.mypy.overrides]]",", starting with the core logic that benefits most.",[61,1184,1185,1188,1189,1191,1192,1191,1194,1196],{},[64,1186,1187],{},"Add rule families one at a time"," — ",[14,1190,80],{},", then ",[14,1193,84],{},[14,1195,88],{}," — each in its own pull request so the discussion stays focused.",[61,1198,1199,1202,1203,1205],{},[64,1200,1201],{},"Add import contracts last",", once the structure they describe exists; ",[14,1204,1079],{}," supports ignoring specific known violations while you untangle them.",[10,1207,1208],{},"Every step lands with CI green, so the checks start protecting new code immediately while old code catches up.",[53,1210,1212],{"id":1211},"a-worked-example-three-findings-three-real-bugs","A worked example: three findings, three real bugs",[10,1214,1215],{},"It is easier to see why these rules earn their place with a concrete case. Here is a plausible module from a deploy tool, written quickly and working fine on its author's Mac:",[176,1217,1219],{"className":484,"code":1218,"language":486,"meta":181,"style":181},"# src\u002Fmytool\u002Fcore\u002Fdeploy.py  (before)\nimport os\nimport subprocess\n\n\ndef deploy(site_dir, host, retries=None):\n    for name in os.listdir(site_dir):\n        print(\"uploading\", name)\n    config = open(os.path.join(site_dir, \"deploy.toml\")).read()\n    subprocess.run(f\"rsync -a {site_dir}\u002F {host}:\u002Fsrv\u002Fsite\", shell=True)\n    return retries + 1\n",[14,1220,1221,1226,1233,1240,1244,1248,1265,1279,1293,1312,1353],{"__ignoreMap":181},[185,1222,1223],{"class":187,"line":188},[185,1224,1225],{"class":227},"# src\u002Fmytool\u002Fcore\u002Fdeploy.py  (before)\n",[185,1227,1228,1230],{"class":187,"line":207},[185,1229,500],{"class":493},[185,1231,1232],{"class":191}," os\n",[185,1234,1235,1237],{"class":187,"line":217},[185,1236,500],{"class":493},[185,1238,1239],{"class":191}," subprocess\n",[185,1241,1242],{"class":187,"line":231},[185,1243,252],{"emptyLinePlaceholder":251},[185,1245,1246],{"class":187,"line":248},[185,1247,252],{"emptyLinePlaceholder":251},[185,1249,1250,1252,1255,1258,1260,1262],{"class":187,"line":255},[185,1251,554],{"class":493},[185,1253,1254],{"class":195}," deploy",[185,1256,1257],{"class":191},"(site_dir, host, retries",[185,1259,538],{"class":493},[185,1261,159],{"class":213},[185,1263,1264],{"class":191},"):\n",[185,1266,1267,1270,1273,1276],{"class":187,"line":273},[185,1268,1269],{"class":493},"    for",[185,1271,1272],{"class":191}," name ",[185,1274,1275],{"class":493},"in",[185,1277,1278],{"class":191}," os.listdir(site_dir):\n",[185,1280,1281,1284,1287,1290],{"class":187,"line":334},[185,1282,1283],{"class":213},"        print",[185,1285,1286],{"class":191},"(",[185,1288,1289],{"class":223},"\"uploading\"",[185,1291,1292],{"class":191},", name)\n",[185,1294,1295,1298,1300,1303,1306,1309],{"class":187,"line":349},[185,1296,1297],{"class":191},"    config ",[185,1299,538],{"class":493},[185,1301,1302],{"class":213}," open",[185,1304,1305],{"class":191},"(os.path.join(site_dir, ",[185,1307,1308],{"class":223},"\"deploy.toml\"",[185,1310,1311],{"class":191},")).read()\n",[185,1313,1314,1317,1319,1322,1324,1327,1329,1332,1334,1337,1339,1342,1344,1347,1349,1351],{"class":187,"line":354},[185,1315,1316],{"class":191},"    subprocess.run(",[185,1318,707],{"class":493},[185,1320,1321],{"class":223},"\"rsync -a ",[185,1323,713],{"class":213},[185,1325,1326],{"class":191},"site_dir",[185,1328,719],{"class":213},[185,1330,1331],{"class":223},"\u002F ",[185,1333,713],{"class":213},[185,1335,1336],{"class":191},"host",[185,1338,719],{"class":213},[185,1340,1341],{"class":223},":\u002Fsrv\u002Fsite\"",[185,1343,240],{"class":191},[185,1345,1346],{"class":613},"shell",[185,1348,538],{"class":493},[185,1350,619],{"class":213},[185,1352,734],{"class":191},[185,1354,1355,1357,1360,1363],{"class":187,"line":376},[185,1356,575],{"class":493},[185,1358,1359],{"class":191}," retries ",[185,1361,1362],{"class":493},"+",[185,1364,1365],{"class":213}," 1\n",[10,1367,1368,1369,1371,1372,1375,1376,1378,1379,240,1382,1115,1385,240,1388,1390,1391,1393,1394,1397,1398,1401,1402,1405,1406,1408,1409,1412,1413,1415,1416,1419],{},"Ruff reports ",[14,1370,152],{}," (a print in core logic, which will corrupt ",[14,1373,1374],{},"--json"," output when a command calls this), ",[14,1377,84],{}," findings for ",[14,1380,1381],{},"os.listdir",[14,1383,1384],{},"open",[14,1386,1387],{},"os.path.join",[14,1389,144],{}," for the unencoded ",[14,1392,148],{}," (a preview rule at the time of writing), ",[14,1395,1396],{},"SIM115"," for opening a file without a context manager, ",[14,1399,1400],{},"F841"," because ",[14,1403,1404],{},"config"," is never used, and ",[14,1407,135],{}," for the shell string — a directory with a space breaks it and one with a semicolon exploits it. mypy, once the function is annotated, reports that ",[14,1410,1411],{},"retries"," may be ",[14,1414,159],{}," when ",[14,1417,1418],{},"+ 1"," is applied. Nearly every finding is a bug some user would eventually hit. The fixed version:",[176,1421,1423],{"className":484,"code":1422,"language":486,"meta":181,"style":181},"# src\u002Fmytool\u002Fcore\u002Fdeploy.py  (after)\nimport logging\nimport subprocess\nfrom pathlib import Path\n\nlog = logging.getLogger(__name__)\n\n\ndef deploy(site_dir: Path, host: str, retries: int = 0) -> int:\n    for path in sorted(site_dir.iterdir()):\n        log.info(\"uploading %s\", path.name)\n    config = (site_dir \u002F \"deploy.toml\").read_text(encoding=\"utf-8\")\n    log.debug(\"deploy config: %d bytes\", len(config))\n    subprocess.run([\"rsync\", \"-a\", \"--\", f\"{site_dir}\u002F\", f\"{host}:\u002Fsrv\u002Fsite\"], check=True)\n    return retries + 1\n",[14,1424,1425,1430,1437,1443,1453,1457,1472,1476,1480,1508,1523,1540,1568,1590,1649],{"__ignoreMap":181},[185,1426,1427],{"class":187,"line":188},[185,1428,1429],{"class":227},"# src\u002Fmytool\u002Fcore\u002Fdeploy.py  (after)\n",[185,1431,1432,1434],{"class":187,"line":207},[185,1433,500],{"class":493},[185,1435,1436],{"class":191}," logging\n",[185,1438,1439,1441],{"class":187,"line":217},[185,1440,500],{"class":493},[185,1442,1239],{"class":191},[185,1444,1445,1447,1449,1451],{"class":187,"line":231},[185,1446,494],{"class":493},[185,1448,497],{"class":191},[185,1450,500],{"class":493},[185,1452,503],{"class":191},[185,1454,1455],{"class":187,"line":248},[185,1456,252],{"emptyLinePlaceholder":251},[185,1458,1459,1462,1464,1467,1470],{"class":187,"line":255},[185,1460,1461],{"class":191},"log ",[185,1463,538],{"class":493},[185,1465,1466],{"class":191}," logging.getLogger(",[185,1468,1469],{"class":213},"__name__",[185,1471,734],{"class":191},[185,1473,1474],{"class":187,"line":273},[185,1475,252],{"emptyLinePlaceholder":251},[185,1477,1478],{"class":187,"line":334},[185,1479,252],{"emptyLinePlaceholder":251},[185,1481,1482,1484,1486,1489,1491,1494,1496,1499,1502,1504,1506],{"class":187,"line":349},[185,1483,554],{"class":493},[185,1485,1254],{"class":195},[185,1487,1488],{"class":191},"(site_dir: Path, host: ",[185,1490,773],{"class":213},[185,1492,1493],{"class":191},", retries: ",[185,1495,24],{"class":213},[185,1497,1498],{"class":493}," =",[185,1500,1501],{"class":213}," 0",[185,1503,671],{"class":191},[185,1505,24],{"class":213},[185,1507,565],{"class":191},[185,1509,1510,1512,1515,1517,1520],{"class":187,"line":354},[185,1511,1269],{"class":493},[185,1513,1514],{"class":191}," path ",[185,1516,1275],{"class":493},[185,1518,1519],{"class":213}," sorted",[185,1521,1522],{"class":191},"(site_dir.iterdir()):\n",[185,1524,1525,1528,1531,1534,1537],{"class":187,"line":376},[185,1526,1527],{"class":191},"        log.info(",[185,1529,1530],{"class":223},"\"uploading ",[185,1532,1533],{"class":213},"%s",[185,1535,1536],{"class":223},"\"",[185,1538,1539],{"class":191},", path.name)\n",[185,1541,1542,1544,1546,1549,1552,1555,1558,1561,1563,1566],{"class":187,"line":401},[185,1543,1297],{"class":191},[185,1545,538],{"class":493},[185,1547,1548],{"class":191}," (site_dir ",[185,1550,1551],{"class":493},"\u002F",[185,1553,1554],{"class":223}," \"deploy.toml\"",[185,1556,1557],{"class":191},").read_text(",[185,1559,1560],{"class":613},"encoding",[185,1562,538],{"class":493},[185,1564,1565],{"class":223},"\"utf-8\"",[185,1567,734],{"class":191},[185,1569,1570,1573,1576,1579,1582,1584,1587],{"class":187,"line":416},[185,1571,1572],{"class":191},"    log.debug(",[185,1574,1575],{"class":223},"\"deploy config: ",[185,1577,1578],{"class":213},"%d",[185,1580,1581],{"class":223}," bytes\"",[185,1583,240],{"class":191},[185,1585,1586],{"class":213},"len",[185,1588,1589],{"class":191},"(config))\n",[185,1591,1592,1595,1598,1600,1603,1605,1608,1610,1612,1614,1616,1618,1620,1623,1625,1627,1629,1631,1633,1635,1637,1640,1643,1645,1647],{"class":187,"line":421},[185,1593,1594],{"class":191},"    subprocess.run([",[185,1596,1597],{"class":223},"\"rsync\"",[185,1599,240],{"class":191},[185,1601,1602],{"class":223},"\"-a\"",[185,1604,240],{"class":191},[185,1606,1607],{"class":223},"\"--\"",[185,1609,240],{"class":191},[185,1611,707],{"class":493},[185,1613,1536],{"class":223},[185,1615,713],{"class":213},[185,1617,1326],{"class":191},[185,1619,719],{"class":213},[185,1621,1622],{"class":223},"\u002F\"",[185,1624,240],{"class":191},[185,1626,707],{"class":493},[185,1628,1536],{"class":223},[185,1630,713],{"class":213},[185,1632,1336],{"class":191},[185,1634,719],{"class":213},[185,1636,1341],{"class":223},[185,1638,1639],{"class":191},"], ",[185,1641,1642],{"class":613},"check",[185,1644,538],{"class":493},[185,1646,619],{"class":213},[185,1648,734],{"class":191},[185,1650,1651,1653,1655,1657],{"class":187,"line":439},[185,1652,575],{"class":493},[185,1654,1359],{"class":191},[185,1656,1362],{"class":493},[185,1658,1365],{"class":213},[10,1660,1661,1662,1664,1665,1668,1669,1671],{},"Narration moved to logging, which the command layer routes to stderr; paths became ",[14,1663,163],{}," objects; the file is read with an explicit encoding; the shell is gone and ",[14,1666,1667],{},"--"," protects against option injection; and the parameter's type makes the ",[14,1670,159],{}," case impossible. None of this required a test, a debugger or a bug report.",[53,1673,1675],{"id":1674},"checks-that-are-not-about-code","Checks that are not about code",[10,1677,1678],{},"Two small checks outside Python source files catch bugs specific to CLI projects and are worth adding to the same lint job:",[58,1680,1681,1709],{},[61,1682,1683,1686,1687,1690,1691,1115,1694,1697,1698,1701,1702,1704,1705,1708],{},[64,1684,1685],{},"Workflow and config files."," ",[14,1688,1689],{},"actionlint"," for GitHub Actions workflows, ",[14,1692,1693],{},"check-yaml",[14,1695,1696],{},"check-toml"," from pre-commit's standard hooks, and ",[14,1699,1700],{},"validate-pyproject"," for ",[14,1703,70],{},", which catches invalid metadata before a build does. A typo in ",[14,1706,1707],{},"[project.scripts]"," is a CLI with no command.",[61,1710,1711,1714,1715,1718,1719,107],{},[64,1712,1713],{},"Help text and documentation."," If your README or docs include ",[14,1716,1717],{},"--help"," output, regenerate and diff it in CI so documentation cannot drift from the real interface. The approach is covered in ",[38,1720,1722],{"href":1721},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli\u002F","generating man pages and docs from a CLI",[53,1724,1726],{"id":1725},"portability-checks-other-platforms-other-pythons","Portability checks: other platforms, other Pythons",[10,1728,1729],{},"A CLI supports a range of Python versions and several operating systems, and static analysis can check some of that range without running anything there.",[10,1731,1732,1735,1736,1738,1739,1742,1743,1746,1747,1750,1751,1754,1755,1758,1759,1763],{},[64,1733,1734],{},"Oldest supported Python."," Set Ruff's ",[14,1737,450],{}," and mypy's ",[14,1740,1741],{},"python_version"," to the oldest version in ",[14,1744,1745],{},"requires-python",". mypy then flags standard-library APIs added later — ",[14,1748,1749],{},"Path.walk()"," (3.12), ",[14,1752,1753],{},"tomllib"," (3.11), ",[14,1756,1757],{},"itertools.batched"," (3.12) — and Ruff avoids suggesting syntax your users cannot run. Pair it with the oldest-version CI job described in ",[38,1760,1762],{"href":1761},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions\u002F","testing a CLI across Python versions with GitHub Actions","; static and runtime checks catch different subsets.",[10,1765,1766,1769,1770,1773,1774,1777,1778,240,1781,240,1783,1786,1787,240,1790,1793,1794,1797,1798,1800,1801,1804,1805,1807],{},[64,1767,1768],{},"Windows."," mypy can check platform-specific branches with ",[14,1771,1772],{},"--platform win32",", verifying that code under ",[14,1775,1776],{},"if sys.platform == \"win32\":"," uses only APIs that exist there. Running mypy once per platform in CI (",[14,1779,1780],{},"mypy --platform linux",[14,1782,1772],{},[14,1784,1785],{},"--platform darwin",") costs seconds and catches calls to ",[14,1788,1789],{},"os.getuid",[14,1791,1792],{},"fcntl"," or ",[14,1795,1796],{},"signal.SIGKILL"," on paths Windows will take. Ruff's ",[14,1799,84],{}," rules push code towards ",[14,1802,1803],{},"pathlib",", which removes the most common source of separator bugs, and ",[14,1806,144],{}," removes the most common source of encoding bugs.",[10,1809,1810,1813,1814,1817],{},[64,1811,1812],{},"Optional dependencies."," If parts of the CLI use extras — ",[14,1815,1816],{},"mytool[s3]"," — type-check with and without them installed, or guard the imports so mypy sees the fallback path. A missing optional import that only fails for users who did not install the extra is a classic late-discovered bug.",[53,1819,1821],{"id":1820},"keeping-the-checks-trusted","Keeping the checks trusted",[10,1823,1824],{},"Static checks only help while people trust them. Three habits keep that trust:",[58,1826,1827,1836,1852],{},[61,1828,1829,1832,1833,107],{},[64,1830,1831],{},"Treat a noisy rule as a bug in the configuration."," If a rule produces mostly false positives in your codebase, disable it with a comment explaining why, rather than littering the code with ",[14,1834,1835],{},"# noqa",[61,1837,1838,1686,1841,1844,1845,1847,1848,1851],{},[64,1839,1840],{},"Make suppressions specific and explained.",[14,1842,1843],{},"# noqa: S603  # argv is a fixed list"," tells the next reader what was considered; a bare ",[14,1846,1835],{}," hides everything on the line. Ruff's ",[14,1849,1850],{},"RUF100"," flags suppressions that no longer suppress anything.",[61,1853,1854,1857],{},[64,1855,1856],{},"Upgrade tools on purpose."," A Dependabot or Renovate pull request that bumps Ruff and shows the new findings in CI is a small, reviewable change. An unpinned tool that changes under you is how teams end up disabling checks in frustration.",[53,1859,1861],{"id":1860},"key-takeaways","Key takeaways",[58,1863,1864,1870,1885,1894,1897],{},[61,1865,1866,1867,1869],{},"Static checks find CLI-specific bugs — shell injection, missing encodings, stray prints, ",[14,1868,159],{}," from optional flags — without writing tests.",[61,1871,1872,1873,240,1875,240,1877,1115,1879,1881,1882,1884],{},"Ruff covers linting and formatting; enable ",[14,1874,80],{},[14,1876,84],{},[14,1878,88],{},[14,1880,92],{}," beyond the defaults, and set ",[14,1883,450],{}," to your oldest Python.",[61,1886,1887,1888,1890,1891,1893],{},"Annotate command parameters with ",[14,1889,740],{},", and give ",[14,1892,102],{}," a real type.",[61,1895,1896],{},"Use import-linter to keep core logic free of CLI frameworks and heavy libraries.",[61,1898,1899],{},"Run one configuration in the editor, in pre-commit and in CI; adopt strictness in stages.",[53,1901,1903],{"id":1902},"frequently-asked-questions","Frequently asked questions",[1905,1906,1908],"h3",{"id":1907},"mypy-or-pyright","mypy or pyright?",[10,1910,1911],{},"Both work well on CLI code. mypy is the long-standing default with a plugin ecosystem and fine-grained per-module configuration; pyright is faster and powers VS Code's Pylance, so many developers already see its errors. Pick one for CI and let people use either in their editor; they agree on the vast majority of real bugs.",[1905,1913,1915],{"id":1914},"do-i-still-need-black-and-isort","Do I still need Black and isort?",[10,1917,1918,1919,1921,1922,1925],{},"No. ",[14,1920,1118],{}," is designed to produce Black-compatible output, and Ruff's ",[14,1923,1924],{},"I"," rules sort imports compatibly with isort's defaults. Replacing them removes two dependencies and two configuration sections.",[1905,1927,1929],{"id":1928},"how-do-i-stop-a-new-rule-from-failing-hundreds-of-existing-lines","How do I stop a new rule from failing hundreds of existing lines?",[10,1931,1932,1933,1936,1937,1940],{},"Enable it with ",[14,1934,1935],{},"--add-noqa",": ",[14,1938,1939],{},"ruff check --select PTH --add-noqa"," inserts a targeted suppression comment on every current violation, so the rule applies to all new code immediately while the existing suppressions become a visible to-do list you can burn down file by file. For mypy, the equivalent is a per-module override that relaxes strictness for legacy modules only.",[1905,1942,1944],{"id":1943},"should-tests-be-type-checked","Should tests be type-checked?",[10,1946,1947,1948,1951],{},"Lightly. Type errors in tests are real bugs too, but requiring full annotations on every test function adds noise. Check tests with ",[14,1949,1950],{},"disallow_untyped_defs = false",", so calls into your typed code are still verified.",[1905,1953,1955],{"id":1954},"what-about-docstring-and-complexity-rules","What about docstring and complexity rules?",[10,1957,1958,1959,1962,1963,240,1966,1969],{},"Ruff offers pydocstyle (",[14,1960,1961],{},"D",") and complexity (",[14,1964,1965],{},"C90",[14,1967,1968],{},"PLR",") rules. Docstring rules are worth enabling on public modules, because Typer and Click use docstrings as help text and a missing one becomes an empty help entry. Complexity limits are useful as a prompt rather than a gate: a command function with a McCabe complexity of twenty usually wants its logic moved into a core module.",[1905,1971,1973],{"id":1972},"will-these-checks-slow-down-development","Will these checks slow down development?",[10,1975,1976,1977,1979],{},"Ruff is fast enough to run on every save and every commit without anyone noticing. mypy on a mid-sized CLI takes a few seconds with its cache warm. ",[14,1978,1079],{}," is similar. If CI lint takes more than a minute, something is misconfigured — usually the cache directory or checking a virtual environment by accident.",[53,1981,1983],{"id":1982},"related","Related",[58,1985,1986,1993,1998,2002,2006,2012],{},[61,1987,1988,1989],{},"Up: ",[38,1990,1992],{"href":1991},"\u002Fproject-setup-dependency-management\u002F","Project Setup & Dependency Management",[61,1994,1995,1996],{},"Down: ",[38,1997,463],{"href":462},[61,1999,1995,2000],{},[38,2001,778],{"href":777},[61,2003,1995,2004],{},[38,2005,1089],{"href":1088},[61,2007,2008,2009],{},"Sideways: ",[38,2010,2011],{"href":40},"Pre-commit hooks for CLI projects",[61,2013,2008,2014],{},[38,2015,46],{"href":45},[2017,2018,2019],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":181,"searchDepth":207,"depth":207,"links":2021},[2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2042],{"id":55,"depth":207,"text":56},{"id":122,"depth":207,"text":123},{"id":167,"depth":207,"text":168},{"id":467,"depth":207,"text":468},{"id":889,"depth":207,"text":890},{"id":1093,"depth":207,"text":1094},{"id":1142,"depth":207,"text":1143},{"id":1211,"depth":207,"text":1212},{"id":1674,"depth":207,"text":1675},{"id":1725,"depth":207,"text":1726},{"id":1820,"depth":207,"text":1821},{"id":1860,"depth":207,"text":1861},{"id":1902,"depth":207,"text":1903,"children":2035},[2036,2037,2038,2039,2040,2041],{"id":1907,"depth":217,"text":1908},{"id":1914,"depth":217,"text":1915},{"id":1928,"depth":217,"text":1929},{"id":1943,"depth":217,"text":1944},{"id":1954,"depth":217,"text":1955},{"id":1972,"depth":217,"text":1973},{"id":1982,"depth":207,"text":1983},"2026-09-18","Set up static checks for a Python CLI: Ruff for linting and formatting, mypy across Click and Typer, import contracts between layers, and where each check runs.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code",{"title":5,"description":2044},"project-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Findex",[201,806,2053,2054,2055],"linting","type-checking","code-quality","Uv_VzMJa0f-3mCcgmOoOF8soa8zld18kuurCfb6w84U",[2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088,2091,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121,2124,2127,2130,2133,2136,2139,2142,2145,2148,2151,2154,2157,2160,2163,2166,2169,2172,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283,2286,2289,2292,2295,2298,2301,2304,2307,2310,2313,2316,2319,2322,2325,2328,2331,2333,2336,2339,2342,2345,2348,2351,2354,2357,2360,2363,2366,2369,2372,2375,2378,2381,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2509,2512,2515,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600],{"path":2059,"title":2060},"\u002Fabout","About Python CLI Toolcraft",{"path":2062,"title":2063},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2065,"title":2066},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2068,"title":2069},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2071,"title":2072},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2074,"title":2075},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2077,"title":2078},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":2080,"title":2081},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":2083,"title":2084},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2086,"title":2087},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2089,"title":2090},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":2092,"title":2093},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":2095,"title":2096},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2098,"title":2099},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2101,"title":2102},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2104,"title":2105},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2107,"title":2108},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2110,"title":2111},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":2113,"title":2114},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2116,"title":2117},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":2119,"title":2120},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2122,"title":2123},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":2125,"title":2126},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2128,"title":2129},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2131,"title":2132},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2134,"title":2135},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":2137,"title":2138},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2140,"title":2141},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":2143,"title":2144},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2146,"title":2147},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":2149,"title":2150},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2152,"title":2153},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2155,"title":2156},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2158,"title":2159},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":2161,"title":2162},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2164,"title":2165},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2167,"title":2168},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2170,"title":2171},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2173,"title":2174},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2176,"title":2177},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2179,"title":2180},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":2182,"title":2183},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2185,"title":2186},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":2188,"title":2189},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2191,"title":2192},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2194,"title":2195},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2197,"title":2198},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2200,"title":2201},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2203,"title":2204},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2206,"title":2207},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2209,"title":2210},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2212,"title":2213},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2215,"title":2216},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2218,"title":2219},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2221,"title":2222},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2224,"title":2225},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2227,"title":2228},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2230,"title":2231},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2233,"title":2234},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2236,"title":2237},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2239,"title":2240},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2242,"title":2243},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2245,"title":2246},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2248,"title":2249},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2251,"title":2252},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2254,"title":2255},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2257,"title":2258},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2260,"title":2261},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2263,"title":2264},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2266,"title":2267},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2269,"title":2270},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2272,"title":2273},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2275,"title":2276},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2278,"title":2279},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2281,"title":2282},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2284,"title":2285},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2287,"title":2288},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2290,"title":2291},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2293,"title":2294},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2296,"title":2297},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2299,"title":2300},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2302,"title":2303},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2305,"title":2306},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2308,"title":2309},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2311,"title":2312},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2314,"title":2315},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2317,"title":2318},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2320,"title":2321},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2323,"title":2324},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2326,"title":2327},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2329,"title":2330},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1551,"title":2332},"Python CLI Toolcraft",{"path":2334,"title":2335},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2337,"title":2338},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2340,"title":2341},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2343,"title":2344},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2346,"title":2347},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2349,"title":2350},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2352,"title":2353},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2355,"title":2356},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2358,"title":2359},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2361,"title":2362},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2364,"title":2365},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2367,"title":2368},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2370,"title":2371},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2373,"title":2374},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2376,"title":2377},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2379,"title":2380},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2382,"title":2383},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2385,"title":2386},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2388,"title":2389},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2391,"title":2392},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2394,"title":2395},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2397,"title":2398},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":2400,"title":2401},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":2403,"title":2404},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2406,"title":2407},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":2409,"title":2410},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2412,"title":2413},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2415,"title":2416},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2418,"title":2419},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2421,"title":2422},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2424,"title":2425},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2427,"title":2428},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":2430,"title":2431},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2433,"title":2434},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2436,"title":2437},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2439,"title":2440},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2442,"title":2443},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":2445,"title":2446},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2448,"title":2449},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":2451,"title":2452},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2454,"title":2455},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2457,"title":2458},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2460,"title":2461},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2463,"title":2464},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2466,"title":2467},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2469,"title":2470},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2472,"title":2473},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2475,"title":2476},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2478,"title":2479},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2481,"title":2482},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2484,"title":2485},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2487,"title":2488},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2490,"title":2491},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2493,"title":2494},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2496,"title":2497},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2499,"title":2500},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2502,"title":2503},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2505,"title":2506},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":2508,"title":1992},"\u002Fproject-setup-dependency-management",{"path":2510,"title":2511},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2513,"title":2514},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2049,"title":5},{"path":2517,"title":2518},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2520,"title":2521},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2523,"title":2524},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2526,"title":2527},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2529,"title":2530},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2532,"title":2533},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2535,"title":2536},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2538,"title":2539},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2541,"title":2542},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2544,"title":2545},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2547,"title":2548},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2550,"title":2551},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2553,"title":2554},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2556,"title":2557},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2559,"title":2560},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2562,"title":2563},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2565,"title":2566},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2568,"title":2569},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":2571,"title":2572},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2574,"title":2575},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2577,"title":2578},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2580,"title":2581},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2583,"title":2584},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":2586,"title":2587},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":2589,"title":2590},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2592,"title":2593},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2595,"title":2596},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2598,"title":2599},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2601,"title":2602},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907492]