[{"data":1,"prerenderedAt":1746},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project\u002F":3,"content-directory":1199},{"id":4,"title":5,"body":6,"date":1186,"description":1187,"difficulty":1188,"draft":1189,"extension":1190,"meta":1191,"navigation":144,"path":1192,"seo":1193,"stem":1194,"tags":1195,"updated":1186,"__hash__":1198},"content\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project\u002Findex.md","Configuring Ruff for a Python CLI Project",{"type":7,"value":8,"toc":1166},"minimark",[9,19,24,47,51,519,542,546,550,608,638,660,681,698,725,729,732,747,792,802,806,809,828,832,855,859,862,902,906,909,944,947,951,954,1046,1056,1060,1075,1079,1088,1095,1099,1106,1110,1117,1121,1128,1132,1162],[10,11,12,13,18],"p",{},"Ruff's defaults — pycodestyle errors and pyflakes — catch undefined names and unused imports, which is useful but misses most of the bugs that actually hurt command-line tools. The rules that matter for a CLI are about shell usage, text encodings, portable paths, and making sure only the command layer writes to the terminal. This guide builds a Ruff configuration for a CLI project rule set by rule set, explains why each matters for command-line code specifically, adds per-path exceptions that encode the project's architecture, sets up formatting, and shows how to adopt it on an existing codebase without a thousand-line cleanup commit. It is part of the ",[14,15,17],"a",{"href":16},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002F","linting and type-checking topic",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,27,40],"ul",{},[28,29,30,31,35,36,39],"li",{},"A CLI project with a ",[32,33,34],"code",{},"pyproject.toml"," and a ",[32,37,38],{},"src\u002F"," layout.",[28,41,42,43,46],{},"Ruff installed as a pinned development dependency: ",[32,44,45],{},"uv add --dev ruff",". Pinning matters — new Ruff releases add rules and occasionally change formatting, and you want those changes to arrive as a reviewed lockfile update.",[20,48,50],{"id":49},"the-recipe-a-cli-oriented-configuration","The recipe: a CLI-oriented configuration",[52,53,58],"pre",{"className":54,"code":55,"language":56,"meta":57,"style":57},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.ruff]\nline-length = 100\ntarget-version = \"py310\"          # the oldest Python in requires-python\nsrc = [\"src\", \"tests\"]\nextend-exclude = [\"scripts\u002Fvendor\"]\n\n[tool.ruff.lint]\nselect = [\n  \"E\", \"W\", \"F\",   # pycodestyle, pyflakes: the baseline\n  \"I\",             # import sorting\n  \"B\",             # bugbear: likely bugs\n  \"UP\",            # pyupgrade: modern syntax for target-version\n  \"S\",             # bandit: security, especially subprocess\n  \"PTH\",           # prefer pathlib to os.path\n  \"T20\",           # print statements\n  \"SIM\",           # simplifications, including unclosed files\n  \"RUF\",           # Ruff's own rules, including unused noqa\n  \"PLW1514\",       # open() in text mode without encoding\n]\nignore = [\n  \"E501\",          # line length is the formatter's job\n  \"S404\",          # importing subprocess is fine; how you call it is checked\n]\npreview = true      # PLW1514 is a preview rule in current Ruff releases\nexplicit-preview-rules = true   # ...but only preview rules named explicitly\n\n[tool.ruff.lint.per-file-ignores]\n\"tests\u002F**\" = [\"S101\", \"S603\", \"S607\", \"T20\"]\n\"src\u002Fmytool\u002Fcli.py\" = [\"T201\"]\n\"src\u002Fmytool\u002Fcommands\u002F**\" = [\"T201\"]\n\"scripts\u002F**\" = [\"T20\", \"S603\", \"S607\"]\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"mytool\"]\n\n[tool.ruff.format]\ndocstring-code-format = true\n","toml","",[32,59,60,69,88,98,111,128,139,146,164,170,192,204,215,227,238,250,261,272,283,295,300,306,318,329,334,346,357,362,384,410,421,431,449,454,476,487,492,510],{"__ignoreMap":57},[61,62,65],"span",{"class":63,"line":64},"line",1,[61,66,68],{"class":67},"sJ8bj","# pyproject.toml\n",[61,70,72,76,80,82,85],{"class":63,"line":71},2,[61,73,75],{"class":74},"sVt8B","[",[61,77,79],{"class":78},"sScJk","tool",[61,81,18],{"class":74},[61,83,84],{"class":78},"ruff",[61,86,87],{"class":74},"]\n",[61,89,91,94],{"class":63,"line":90},3,[61,92,93],{"class":74},"line-length = ",[61,95,97],{"class":96},"sj4cs","100\n",[61,99,101,104,108],{"class":63,"line":100},4,[61,102,103],{"class":74},"target-version = ",[61,105,107],{"class":106},"sZZnC","\"py310\"",[61,109,110],{"class":67},"          # the oldest Python in requires-python\n",[61,112,114,117,120,123,126],{"class":63,"line":113},5,[61,115,116],{"class":74},"src = [",[61,118,119],{"class":106},"\"src\"",[61,121,122],{"class":74},", ",[61,124,125],{"class":106},"\"tests\"",[61,127,87],{"class":74},[61,129,131,134,137],{"class":63,"line":130},6,[61,132,133],{"class":74},"extend-exclude = [",[61,135,136],{"class":106},"\"scripts\u002Fvendor\"",[61,138,87],{"class":74},[61,140,142],{"class":63,"line":141},7,[61,143,145],{"emptyLinePlaceholder":144},true,"\n",[61,147,149,151,153,155,157,159,162],{"class":63,"line":148},8,[61,150,75],{"class":74},[61,152,79],{"class":78},[61,154,18],{"class":74},[61,156,84],{"class":78},[61,158,18],{"class":74},[61,160,161],{"class":78},"lint",[61,163,87],{"class":74},[61,165,167],{"class":63,"line":166},9,[61,168,169],{"class":74},"select = [\n",[61,171,173,176,178,181,183,186,189],{"class":63,"line":172},10,[61,174,175],{"class":106},"  \"E\"",[61,177,122],{"class":74},[61,179,180],{"class":106},"\"W\"",[61,182,122],{"class":74},[61,184,185],{"class":106},"\"F\"",[61,187,188],{"class":74},",   ",[61,190,191],{"class":67},"# pycodestyle, pyflakes: the baseline\n",[61,193,195,198,201],{"class":63,"line":194},11,[61,196,197],{"class":106},"  \"I\"",[61,199,200],{"class":74},",             ",[61,202,203],{"class":67},"# import sorting\n",[61,205,207,210,212],{"class":63,"line":206},12,[61,208,209],{"class":106},"  \"B\"",[61,211,200],{"class":74},[61,213,214],{"class":67},"# bugbear: likely bugs\n",[61,216,218,221,224],{"class":63,"line":217},13,[61,219,220],{"class":106},"  \"UP\"",[61,222,223],{"class":74},",            ",[61,225,226],{"class":67},"# pyupgrade: modern syntax for target-version\n",[61,228,230,233,235],{"class":63,"line":229},14,[61,231,232],{"class":106},"  \"S\"",[61,234,200],{"class":74},[61,236,237],{"class":67},"# bandit: security, especially subprocess\n",[61,239,241,244,247],{"class":63,"line":240},15,[61,242,243],{"class":106},"  \"PTH\"",[61,245,246],{"class":74},",           ",[61,248,249],{"class":67},"# prefer pathlib to os.path\n",[61,251,253,256,258],{"class":63,"line":252},16,[61,254,255],{"class":106},"  \"T20\"",[61,257,246],{"class":74},[61,259,260],{"class":67},"# print statements\n",[61,262,264,267,269],{"class":63,"line":263},17,[61,265,266],{"class":106},"  \"SIM\"",[61,268,246],{"class":74},[61,270,271],{"class":67},"# simplifications, including unclosed files\n",[61,273,275,278,280],{"class":63,"line":274},18,[61,276,277],{"class":106},"  \"RUF\"",[61,279,246],{"class":74},[61,281,282],{"class":67},"# Ruff's own rules, including unused noqa\n",[61,284,286,289,292],{"class":63,"line":285},19,[61,287,288],{"class":106},"  \"PLW1514\"",[61,290,291],{"class":74},",       ",[61,293,294],{"class":67},"# open() in text mode without encoding\n",[61,296,298],{"class":63,"line":297},20,[61,299,87],{"class":74},[61,301,303],{"class":63,"line":302},21,[61,304,305],{"class":74},"ignore = [\n",[61,307,309,312,315],{"class":63,"line":308},22,[61,310,311],{"class":106},"  \"E501\"",[61,313,314],{"class":74},",          ",[61,316,317],{"class":67},"# line length is the formatter's job\n",[61,319,321,324,326],{"class":63,"line":320},23,[61,322,323],{"class":106},"  \"S404\"",[61,325,314],{"class":74},[61,327,328],{"class":67},"# importing subprocess is fine; how you call it is checked\n",[61,330,332],{"class":63,"line":331},24,[61,333,87],{"class":74},[61,335,337,340,343],{"class":63,"line":336},25,[61,338,339],{"class":74},"preview = ",[61,341,342],{"class":96},"true",[61,344,345],{"class":67},"      # PLW1514 is a preview rule in current Ruff releases\n",[61,347,349,352,354],{"class":63,"line":348},26,[61,350,351],{"class":74},"explicit-preview-rules = ",[61,353,342],{"class":96},[61,355,356],{"class":67},"   # ...but only preview rules named explicitly\n",[61,358,360],{"class":63,"line":359},27,[61,361,145],{"emptyLinePlaceholder":144},[61,363,365,367,369,371,373,375,377,379,382],{"class":63,"line":364},28,[61,366,75],{"class":74},[61,368,79],{"class":78},[61,370,18],{"class":74},[61,372,84],{"class":78},[61,374,18],{"class":74},[61,376,161],{"class":78},[61,378,18],{"class":74},[61,380,381],{"class":78},"per-file-ignores",[61,383,87],{"class":74},[61,385,387,390,393,395,398,400,403,405,408],{"class":63,"line":386},29,[61,388,389],{"class":74},"\"tests\u002F**\" = [",[61,391,392],{"class":106},"\"S101\"",[61,394,122],{"class":74},[61,396,397],{"class":106},"\"S603\"",[61,399,122],{"class":74},[61,401,402],{"class":106},"\"S607\"",[61,404,122],{"class":74},[61,406,407],{"class":106},"\"T20\"",[61,409,87],{"class":74},[61,411,413,416,419],{"class":63,"line":412},30,[61,414,415],{"class":74},"\"src\u002Fmytool\u002Fcli.py\" = [",[61,417,418],{"class":106},"\"T201\"",[61,420,87],{"class":74},[61,422,424,427,429],{"class":63,"line":423},31,[61,425,426],{"class":74},"\"src\u002Fmytool\u002Fcommands\u002F**\" = [",[61,428,418],{"class":106},[61,430,87],{"class":74},[61,432,434,437,439,441,443,445,447],{"class":63,"line":433},32,[61,435,436],{"class":74},"\"scripts\u002F**\" = [",[61,438,407],{"class":106},[61,440,122],{"class":74},[61,442,397],{"class":106},[61,444,122],{"class":74},[61,446,402],{"class":106},[61,448,87],{"class":74},[61,450,452],{"class":63,"line":451},33,[61,453,145],{"emptyLinePlaceholder":144},[61,455,457,459,461,463,465,467,469,471,474],{"class":63,"line":456},34,[61,458,75],{"class":74},[61,460,79],{"class":78},[61,462,18],{"class":74},[61,464,84],{"class":78},[61,466,18],{"class":74},[61,468,161],{"class":78},[61,470,18],{"class":74},[61,472,473],{"class":78},"isort",[61,475,87],{"class":74},[61,477,479,482,485],{"class":63,"line":478},35,[61,480,481],{"class":74},"known-first-party = [",[61,483,484],{"class":106},"\"mytool\"",[61,486,87],{"class":74},[61,488,490],{"class":63,"line":489},36,[61,491,145],{"emptyLinePlaceholder":144},[61,493,495,497,499,501,503,505,508],{"class":63,"line":494},37,[61,496,75],{"class":74},[61,498,79],{"class":78},[61,500,18],{"class":74},[61,502,84],{"class":78},[61,504,18],{"class":74},[61,506,507],{"class":78},"format",[61,509,87],{"class":74},[61,511,513,516],{"class":63,"line":512},38,[61,514,515],{"class":74},"docstring-code-format = ",[61,517,518],{"class":96},"true\n",[10,520,521,522,525,526,529,530,534,535,538,539,18],{},"The ",[32,523,524],{},"preview"," pair deserves a note: ",[32,527,528],{},"preview = true"," alone would enable ",[531,532,533],"em",{},"every"," preview rule in the selected families, which change between releases; ",[32,536,537],{},"explicit-preview-rules = true"," limits it to preview rules you name exactly, such as ",[32,540,541],{},"PLW1514",[20,543,545],{"id":544},"why-these-rule-sets-for-a-cli","Why these rule sets, for a CLI",[547,548],"inline-diagram",{"name":549},"lint-ruff-rules",[10,551,552,559,560,563,564,122,567,570,571,563,574,577,578,563,581,584,585,563,588,591,592,594,595,599,600,603,604,607],{},[553,554,555,558],"strong",{},[32,556,557],{},"S"," (security)."," The flake8-bandit rules find subprocess calls with ",[32,561,562],{},"shell=True"," (",[32,565,566],{},"S602",[32,568,569],{},"S604","), ",[32,572,573],{},"os.system",[32,575,576],{},"S605","), unsafe ",[32,579,580],{},"yaml.load",[32,582,583],{},"S506","), hard-coded passwords and temporary paths like ",[32,586,587],{},"\u002Ftmp\u002Ffoo",[32,589,590],{},"S108","). For a CLI that shells out, ",[32,593,566],{}," alone justifies the family — it flags exactly the pattern described in ",[14,596,598],{"href":597},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis\u002F","avoiding shell injection in Python CLIs",". ",[32,601,602],{},"S404"," (importing ",[32,605,606],{},"subprocess"," at all) is noise for a tool whose job involves running programs, which is why it is ignored.",[10,609,610,616,617,122,620,122,623,626,627,630,631,633,634,18],{},[553,611,612,615],{},[32,613,614],{},"PTH"," (pathlib)."," Flags ",[32,618,619],{},"os.path.join",[32,621,622],{},"os.listdir",[32,624,625],{},"open()"," and friends in favour of ",[32,628,629],{},"pathlib"," equivalents. Path-handling code written with ",[32,632,629],{}," has far fewer platform bugs, as discussed in ",[14,635,637],{"href":636},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002F","cross-platform paths with pathlib",[10,639,640,645,646,122,648,651,652,655,656,659],{},[553,641,642,644],{},[32,643,541],{}," (unspecified encoding)."," Flags text-mode ",[32,647,625],{},[32,649,650],{},"read_text()"," and friends without ",[32,653,654],{},"encoding=",". Until UTF-8 mode is the default everywhere, these use the locale encoding — often ",[32,657,658],{},"cp1252"," on Windows — and are the single most common source of Windows-only CLI bugs.",[10,661,662,668,669,672,673,676,677,680],{},[553,663,664,667],{},[32,665,666],{},"T20"," (print)."," In a CLI, printing is a deliberate act of the command layer: results to stdout, diagnostics to stderr. A ",[32,670,671],{},"print()"," in a core module writes to stdout behind the command's back, which breaks ",[32,674,675],{},"--json"," output and anything piping your tool. Ignoring ",[32,678,679],{},"T201"," only in command modules encodes that rule.",[10,682,683,689,690,693,694,697],{},[553,684,685,688],{},[32,686,687],{},"B"," (bugbear)."," Catches mutable default arguments (common in hand-written option defaults), ",[32,691,692],{},"except"," clauses that hide ",[32,695,696],{},"KeyboardInterrupt",", and loop-variable closure bugs in callbacks.",[10,699,700,706,707,710,711,122,714,710,717,720,721,724],{},[553,701,702,705],{},[32,703,704],{},"UP"," (pyupgrade), with the right target."," Rewrites old syntax — ",[32,708,709],{},"Optional[X]"," to ",[32,712,713],{},"X | None",[32,715,716],{},"typing.List",[32,718,719],{},"list"," — but only as far as ",[32,722,723],{},"target-version"," allows. Set it to your oldest supported Python, or Ruff will suggest syntax your users cannot run.",[20,726,728],{"id":727},"what-it-finds-in-practice","What it finds in practice",[547,730],{"name":731},"lint-ruff-terminal",[10,733,734,735,738,739,742,743,746],{},"Run ",[32,736,737],{},"uv run ruff check"," and you will usually see a handful of findings in each category on an existing CLI. Most have safe automatic fixes (",[32,740,741],{},"ruff check --fix","); security findings and encoding findings deserve a human look, because the right fix depends on what the code means. For example, a subprocess call flagged with ",[32,744,745],{},"S603"," (\"check for untrusted input\") may be perfectly fine with a fixed argument list — in which case a specific, explained suppression is the right response:",[52,748,752],{"className":749,"code":750,"language":751,"meta":57,"style":57},"language-python shiki shiki-themes github-light github-dark","subprocess.run([\"git\", \"rev-parse\", \"HEAD\"], check=True)  # noqa: S603  (fixed argv)\n","python",[32,753,754],{"__ignoreMap":57},[61,755,756,759,762,764,767,769,772,775,779,783,786,789],{"class":63,"line":64},[61,757,758],{"class":74},"subprocess.run([",[61,760,761],{"class":106},"\"git\"",[61,763,122],{"class":74},[61,765,766],{"class":106},"\"rev-parse\"",[61,768,122],{"class":74},[61,770,771],{"class":106},"\"HEAD\"",[61,773,774],{"class":74},"], ",[61,776,778],{"class":777},"s4XuR","check",[61,780,782],{"class":781},"szBVR","=",[61,784,785],{"class":96},"True",[61,787,788],{"class":74},")  ",[61,790,791],{"class":67},"# noqa: S603  (fixed argv)\n",[10,793,794,797,798,801],{},[32,795,796],{},"RUF100"," then flags that ",[32,799,800],{},"noqa"," if a later refactor makes it unnecessary, so suppressions do not accumulate silently.",[20,803,805],{"id":804},"per-path-ignores-encode-the-architecture","Per-path ignores encode the architecture",[547,807],{"name":808},"lint-per-file",[10,810,521,811,813,814,817,818,710,821,824,825,827],{},[32,812,381],{}," table is more than a list of exceptions: it states where each kind of code is allowed to live. Tests may use ",[32,815,816],{},"assert"," and run subprocesses. The command layer may print. Everything else must go through logging or return values. When someone adds a ",[32,819,820],{},"print",[32,822,823],{},"src\u002Fmytool\u002Fcore\u002Fplan.py",", the linter explains the project's rule for you. Prefer ignoring by path over sprinkling ",[32,826,800],{}," comments; a path rule documents a decision once, whereas scattered comments hide it.",[20,829,831],{"id":830},"formatting","Formatting",[10,833,834,837,838,841,842,844,845,847,848,850,851,854],{},[32,835,836],{},"ruff format"," is a Black-compatible formatter, so if the project used Black, switching changes almost nothing. With ",[32,839,840],{},"docstring-code-format = true"," it also formats code examples inside docstrings — useful for CLIs, whose command docstrings often double as help text with usage examples. Run ",[32,843,836],{}," before ",[32,846,741],{}," in hooks, or let ",[32,849,741],{}," handle import sorting (",[32,852,853],{},"I",") and the formatter handle layout; they are designed not to fight.",[20,856,858],{"id":857},"ux-considerations","UX considerations",[10,860,861],{},"The \"users\" here are contributors, and the goal is a configuration they barely notice:",[25,863,864,870,886,896],{},[28,865,866,869],{},[553,867,868],{},"Fast feedback."," Enable the Ruff language server (or editor extension) so findings appear on save. Ruff is fast enough that there is no reason to wait for CI.",[28,871,872,875,876,878,879,881,882,18],{},[553,873,874],{},"Autofix on commit."," A pre-commit hook running ",[32,877,741],{}," and ",[32,880,836],{}," on staged files turns most findings into non-events. See ",[14,883,885],{"href":884},"\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",[28,887,888,891,892,895],{},[553,889,890],{},"Messages that teach."," ",[32,893,894],{},"ruff rule S602"," prints the rationale and examples for any rule, which is a better answer to \"why is this flagged?\" than a link to a wiki page.",[28,897,898,901],{},[553,899,900],{},"Few surprises."," Pin the version, review upgrades like any dependency, and add new rule families one pull request at a time.",[20,903,905],{"id":904},"adopting-it-on-an-existing-codebase","Adopting it on an existing codebase",[10,907,908],{},"Turning this configuration on in a mature CLI may produce hundreds of findings. Rather than a single enormous fix-up commit that nobody can review, stage it:",[910,911,912,920,926,933],"ol",{},[28,913,734,914,916,917,18],{},[32,915,836],{}," once, commit it alone, and add the commit hash to ",[32,918,919],{},".git-blame-ignore-revs",[28,921,922,923,925],{},"Apply safe automatic fixes: ",[32,924,741],{},", review, commit.",[28,927,928,929,932],{},"Suppress the remaining findings in place with ",[32,930,931],{},"ruff check --add-noqa",", commit. The configuration is now enforced for all new code.",[28,934,935,936,938,939,878,941,943],{},"Burn down the inserted ",[32,937,800],{}," comments file by file in ordinary pull requests, starting with ",[32,940,557],{},[32,942,541],{},", which are the most likely to be real bugs.",[10,945,946],{},"This keeps CI green from the first commit and makes every later change small and reviewable.",[20,948,950],{"id":949},"testing-the-behaviour","Testing the behaviour",[10,952,953],{},"A linter configuration is itself something you can test. Two quick checks catch most mistakes:",[52,955,959],{"className":956,"code":957,"language":958,"meta":57,"style":57},"language-bash shiki shiki-themes github-light github-dark","# 1. Does the configuration parse, and which rules are actually active?\nuv run ruff check --show-settings src\u002Fmytool\u002Fcli.py | grep -A3 \"linter.rules.enabled\" | head\n\n# 2. Does it catch what it should? Lint a deliberately bad snippet.\nprintf 'import subprocess\\nsubprocess.run(f\"ls {x}\", shell=True)\\nprint(open(\"f\").read())\\n' \\\n  | uv run ruff check --stdin-filename src\u002Fmytool\u002Fcore\u002Fprobe.py -\n","bash",[32,960,961,966,1003,1007,1012,1023],{"__ignoreMap":57},[61,962,963],{"class":63,"line":64},[61,964,965],{"class":67},"# 1. Does the configuration parse, and which rules are actually active?\n",[61,967,968,971,974,977,980,983,986,989,992,995,998,1000],{"class":63,"line":71},[61,969,970],{"class":78},"uv",[61,972,973],{"class":106}," run",[61,975,976],{"class":106}," ruff",[61,978,979],{"class":106}," check",[61,981,982],{"class":96}," --show-settings",[61,984,985],{"class":106}," src\u002Fmytool\u002Fcli.py",[61,987,988],{"class":781}," |",[61,990,991],{"class":78}," grep",[61,993,994],{"class":96}," -A3",[61,996,997],{"class":106}," \"linter.rules.enabled\"",[61,999,988],{"class":781},[61,1001,1002],{"class":78}," head\n",[61,1004,1005],{"class":63,"line":90},[61,1006,145],{"emptyLinePlaceholder":144},[61,1008,1009],{"class":63,"line":100},[61,1010,1011],{"class":67},"# 2. Does it catch what it should? Lint a deliberately bad snippet.\n",[61,1013,1014,1017,1020],{"class":63,"line":113},[61,1015,1016],{"class":96},"printf",[61,1018,1019],{"class":106}," 'import subprocess\\nsubprocess.run(f\"ls {x}\", shell=True)\\nprint(open(\"f\").read())\\n'",[61,1021,1022],{"class":96}," \\\n",[61,1024,1025,1028,1031,1033,1035,1037,1040,1043],{"class":63,"line":130},[61,1026,1027],{"class":781},"  |",[61,1029,1030],{"class":78}," uv",[61,1032,973],{"class":106},[61,1034,976],{"class":106},[61,1036,979],{"class":106},[61,1038,1039],{"class":96}," --stdin-filename",[61,1041,1042],{"class":106}," src\u002Fmytool\u002Fcore\u002Fprobe.py",[61,1044,1045],{"class":106}," -\n",[10,1047,1048,1049,1051,1052,1055],{},"The second command should report the shell call, the print outside the command layer, the unencoded ",[32,1050,625],{}," and the missing context manager. Repeating it with ",[32,1053,1054],{},"--stdin-filename src\u002Fmytool\u002Fcli.py"," should drop the print finding, proving the per-path ignore works. Wiring a snippet like this into a test is overkill for most projects, but running it once after changing the configuration is a good habit.",[20,1057,1059],{"id":1058},"conclusion","Conclusion",[10,1061,1062,1063,122,1065,122,1067,878,1069,1071,1072,1074],{},"A CLI's most damaging bugs — shell injection, missing encodings, non-portable paths, stray output — are exactly what Ruff's ",[32,1064,557],{},[32,1066,541],{},[32,1068,614],{},[32,1070,666],{}," rules catch. Add them to the baseline, set ",[32,1073,723],{}," to your oldest Python, encode your architecture in per-path ignores, pin Ruff, and adopt the configuration in stages so every commit stays green. The result is a codebase where a whole category of bug reports simply stops arriving.",[20,1076,1078],{"id":1077},"frequently-asked-questions","Frequently asked questions",[1080,1081,1083,1084,1087],"h3",{"id":1082},"should-i-select-all-and-ignore-what-i-do-not-want","Should I select ",[32,1085,1086],{},"ALL"," and ignore what I do not want?",[10,1089,1090,1091,1094],{},"Some teams do, and it is a legitimate way to discover rules. The cost is that every Ruff upgrade can enable new rules that fail CI. Selecting families explicitly gives more predictable upgrades; periodically running with ",[32,1092,1093],{},"--select ALL"," in a scratch branch is a good way to find families worth adding.",[1080,1096,1098],{"id":1097},"what-line-length-should-a-cli-project-use","What line length should a CLI project use?",[10,1100,1101,1102,1105],{},"Anything between 88 and 120 works; consistency matters more than the number. Help text strings are the usual source of long lines in CLI code — the formatter will not split strings, so rely on implicit string concatenation or ",[32,1103,1104],{},"textwrap.dedent"," for long help.",[1080,1107,1109],{"id":1108},"does-ruff-replace-mypy","Does Ruff replace mypy?",[10,1111,1112,1113,18],{},"No. Ruff checks syntax-level patterns and a few type-adjacent issues, but it does not do type inference across modules. Use both; they are complementary. See ",[14,1114,1116],{"href":1115},"\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",[1080,1118,1120],{"id":1119},"how-do-i-handle-generated-code","How do I handle generated code?",[10,1122,1123,1124,1127],{},"Exclude it with ",[32,1125,1126],{},"extend-exclude",", or apply per-file ignores to the generated directory. Linting code you do not edit by hand only produces noise.",[20,1129,1131],{"id":1130},"related","Related",[25,1133,1134,1140,1145,1151,1156],{},[28,1135,1136,1137],{},"Up: ",[14,1138,1139],{"href":16},"Linting and type-checking CLI code",[28,1141,1142],{},[14,1143,1144],{"href":1115},"Type-checking Click and Typer code with mypy",[28,1146,1147],{},[14,1148,1150],{"href":1149},"\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",[28,1152,1153],{},[14,1154,1155],{"href":884},"Setting up pre-commit for Python CLI repos",[28,1157,1158],{},[14,1159,1161],{"href":1160},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python\u002F","Writing local pre-commit hooks in Python",[1163,1164,1165],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":57,"searchDepth":71,"depth":71,"links":1167},[1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1185],{"id":22,"depth":71,"text":23},{"id":49,"depth":71,"text":50},{"id":544,"depth":71,"text":545},{"id":727,"depth":71,"text":728},{"id":804,"depth":71,"text":805},{"id":830,"depth":71,"text":831},{"id":857,"depth":71,"text":858},{"id":904,"depth":71,"text":905},{"id":949,"depth":71,"text":950},{"id":1058,"depth":71,"text":1059},{"id":1077,"depth":71,"text":1078,"children":1179},[1180,1182,1183,1184],{"id":1082,"depth":90,"text":1181},"Should I select ALL and ignore what I do not want?",{"id":1097,"depth":90,"text":1098},{"id":1108,"depth":90,"text":1109},{"id":1119,"depth":90,"text":1120},{"id":1130,"depth":71,"text":1131},"2026-09-18","A Ruff configuration tuned for Python CLIs: rule sets for subprocess safety, pathlib and stray prints, per-path ignores, formatting, and adopting it without a huge diff.","beginner",false,"md",{},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project",{"title":5,"description":1187},"project-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project\u002Findex",[84,1196,830,1197],"linting","code-quality","KbWwR3GSmMjAzvJvk0zQu4A3IiAiHuZ1KpK81kmpP8I",[1200,1203,1206,1209,1212,1215,1218,1221,1224,1227,1230,1233,1236,1239,1242,1245,1248,1251,1254,1257,1260,1263,1266,1269,1272,1275,1278,1281,1284,1287,1290,1293,1296,1299,1302,1305,1308,1311,1314,1317,1320,1323,1326,1329,1332,1335,1338,1341,1344,1347,1350,1353,1356,1359,1362,1365,1368,1371,1374,1377,1380,1383,1386,1389,1392,1395,1398,1401,1404,1407,1410,1413,1416,1419,1422,1425,1428,1431,1434,1437,1440,1443,1446,1449,1452,1455,1458,1461,1464,1467,1470,1473,1476,1479,1482,1485,1488,1491,1494,1497,1500,1503,1506,1509,1512,1515,1518,1521,1524,1527,1530,1533,1536,1539,1542,1545,1548,1551,1554,1557,1560,1563,1566,1569,1572,1575,1578,1581,1584,1587,1590,1593,1596,1599,1602,1605,1608,1611,1614,1617,1620,1623,1626,1629,1632,1635,1638,1641,1644,1647,1650,1653,1654,1657,1660,1663,1666,1669,1672,1675,1678,1681,1684,1687,1690,1693,1696,1699,1702,1705,1708,1711,1713,1716,1719,1722,1725,1728,1731,1734,1737,1740,1743],{"path":1201,"title":1202},"\u002Fabout","About Python CLI Toolcraft",{"path":1204,"title":1205},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1207,"title":1208},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1210,"title":1211},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1213,"title":1214},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1216,"title":1217},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1219,"title":1220},"\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":1222,"title":1223},"\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":1225,"title":1226},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1228,"title":1229},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1231,"title":1232},"\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":1234,"title":1235},"\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":1237,"title":1238},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1240,"title":1241},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1243,"title":1244},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1246,"title":1247},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1249,"title":1250},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1252,"title":1253},"\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":1255,"title":1256},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1258,"title":1259},"\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":1261,"title":1262},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1264,"title":1265},"\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":1267,"title":1268},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1270,"title":1271},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1273,"title":1274},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1276,"title":1277},"\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":1279,"title":1280},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1282,"title":1283},"\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":1285,"title":1286},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1288,"title":1289},"\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":1291,"title":1292},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1294,"title":1295},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1297,"title":1298},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1300,"title":1301},"\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":1303,"title":1304},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1306,"title":1307},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1309,"title":1310},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1312,"title":1313},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1315,"title":1316},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1318,"title":1319},"\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":1321,"title":1322},"\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":1324,"title":1325},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1327,"title":1328},"\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":1330,"title":1331},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1333,"title":1334},"\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":1336,"title":1337},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1339,"title":1340},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1342,"title":1343},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1345,"title":1346},"\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":1348,"title":1349},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1351,"title":1352},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1354,"title":1355},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1357,"title":1358},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1360,"title":1361},"\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":1363,"title":1364},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1366,"title":1367},"\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":1369,"title":1370},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1372,"title":1373},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1375,"title":1376},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1378,"title":1379},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1381,"title":1382},"\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":1384,"title":1385},"\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":1387,"title":1388},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1390,"title":1391},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1393,"title":1394},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1396,"title":1397},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1399,"title":1400},"\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":1402,"title":1403},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1405,"title":1406},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1408,"title":1409},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1411,"title":1412},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1414,"title":1415},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1417,"title":1418},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1420,"title":1421},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1423,"title":1424},"\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":1426,"title":1427},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1429,"title":1430},"\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":1432,"title":1433},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1435,"title":1436},"\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":1438,"title":1439},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1441,"title":1442},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1444,"title":1445},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1447,"title":1448},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1450,"title":1451},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1453,"title":1454},"\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":1456,"title":1457},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1459,"title":1460},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1462,"title":1463},"\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":1465,"title":1466},"\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":1468,"title":1469},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1471,"title":1472},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1474,"title":1475},"\u002F","Python CLI Toolcraft",{"path":1477,"title":1478},"\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":1480,"title":1481},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1483,"title":1484},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1486,"title":1487},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1489,"title":1490},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1492,"title":1493},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1495,"title":1496},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1498,"title":1499},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1501,"title":1502},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1504,"title":1505},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1507,"title":1508},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1510,"title":1511},"\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":1513,"title":1514},"\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":1516,"title":1517},"\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":1519,"title":1520},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1522,"title":1523},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1525,"title":1526},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1528,"title":1529},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1531,"title":1532},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1534,"title":1535},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1537,"title":1538},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1540,"title":1541},"\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":1543,"title":1544},"\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":1546,"title":1547},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1549,"title":1550},"\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":1552,"title":1553},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1555,"title":1556},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1558,"title":1559},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1561,"title":1562},"\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":1564,"title":1565},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1567,"title":1568},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1570,"title":1571},"\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":1573,"title":1574},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1576,"title":1577},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1579,"title":1580},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1582,"title":1583},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1585,"title":1586},"\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":1588,"title":1589},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1591,"title":1592},"\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":1594,"title":1595},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1597,"title":1598},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1600,"title":1601},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":1603,"title":1604},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":1606,"title":1607},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":1609,"title":1610},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":1612,"title":1613},"\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":1615,"title":1616},"\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":1618,"title":1619},"\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":1621,"title":1622},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":1624,"title":1625},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1627,"title":1628},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1630,"title":1631},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":1633,"title":1634},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1636,"title":1637},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1639,"title":1640},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1642,"title":1643},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1645,"title":1646},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":1648,"title":1649},"\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":1651,"title":1652},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1192,"title":5},{"path":1655,"title":1656},"\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":1658,"title":1659},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":1661,"title":1662},"\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":1664,"title":1665},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1667,"title":1668},"\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":1670,"title":1671},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1673,"title":1674},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1676,"title":1677},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":1679,"title":1680},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1682,"title":1683},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":1685,"title":1686},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1688,"title":1689},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1691,"title":1692},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1694,"title":1695},"\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":1697,"title":1698},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1700,"title":1701},"\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":1703,"title":1704},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":1706,"title":1707},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1709,"title":1710},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1712,"title":1155},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos",{"path":1714,"title":1715},"\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":1717,"title":1718},"\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":1720,"title":1721},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1723,"title":1724},"\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":1726,"title":1727},"\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":1729,"title":1730},"\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":1732,"title":1733},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":1735,"title":1736},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1738,"title":1739},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1741,"title":1742},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":1744,"title":1745},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907489]