[{"data":1,"prerenderedAt":1714},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis\u002F":3,"content-directory":1468},{"id":4,"title":5,"body":6,"date":1453,"description":1454,"difficulty":1455,"draft":1456,"extension":1457,"meta":1458,"navigation":194,"path":1459,"seo":1460,"stem":1461,"tags":1462,"updated":1453,"__hash__":1467},"content\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis\u002Findex.md","Poetry Entry Points and Scripts for CLIs",{"type":7,"value":8,"toc":1431},"minimark",[9,34,39,99,103,109,113,258,281,291,440,451,570,585,591,601,731,771,777,783,786,871,903,911,914,920,955,966,1036,1059,1063,1066,1069,1108,1125,1136,1199,1213,1217,1253,1257,1343,1347,1360,1369,1373,1376,1380,1383,1387,1390,1394,1397,1401,1427],[10,11,12,13,17,18,21,22,25,26,29,30,33],"p",{},"An entry point is the line of metadata that turns your Python package into a command someone can type. Without it, users have to run ",[14,15,16],"code",{},"python -m yourpackage","; with it, they type ",[14,19,20],{},"yourcli"," and it just works. Poetry declares these console scripts in a small table in ",[14,23,24],{},"pyproject.toml",", and ",[14,27,28],{},"poetry install"," generates the wrapper executable that puts your command on ",[14,31,32],{},"PATH",". This guide shows the exact syntax, both the Poetry-specific and the standards-based table, and how to verify the command actually resolves before you publish.",[35,36,38],"h2",{"id":37},"tldr","TL;DR",[40,41,42,54,66,74,85],"ul",{},[43,44,45,46,49,50,53],"li",{},"Map a command to a callable in ",[14,47,48],{},"[tool.poetry.scripts]",": ",[14,51,52],{},"mycli = \"mypackage.cli:main\"",".",[43,55,56,57,61,62,65],{},"On ",[58,59,60],"strong",{},"poetry-core >= 2.0"," you can (and should) use the standard PEP 621 ",[14,63,64],{},"[project.scripts]"," table instead — same syntax, tool-agnostic.",[43,67,68,70,71,73],{},[14,69,28],{}," creates the console-script wrapper; the command is then on ",[14,72,32],{}," inside the project environment.",[43,75,76,77,80,81,84],{},"Test it without publishing: ",[14,78,79],{},"poetry run mycli",", or activate the environment and call ",[14,82,83],{},"mycli"," directly.",[43,86,87,88,91,92,95,96,53],{},"The target callable takes ",[58,89,90],{},"no arguments"," — Click and Typer read ",[14,93,94],{},"sys.argv"," themselves. Don't write ",[14,97,98],{},"main(sys.argv)",[35,100,102],{"id":101},"the-scripts-table-mapping-a-command-to-a-callable","The scripts table: mapping a command to a callable",[10,104,105,106,108],{},"The console-script declaration has one job: bind a command name to an importable callable. Poetry's historical table is ",[14,107,48],{},":",[110,111],"inline-diagram",{"name":112},"poetry-scripts-mapping",[114,115,120],"pre",{"className":116,"code":117,"language":118,"meta":119,"style":119},"language-toml shiki shiki-themes github-light github-dark","[tool.poetry]\nname = \"mycli\"\nversion = \"0.1.0\"\ndescription = \"A friendly command-line tool.\"\npackages = [{ include = \"mycli\", from = \"src\" }]\n\n[tool.poetry.scripts]\nmycli = \"mycli.cli:app\"\n\n[build-system]\nrequires = [\"poetry-core>=1.8\"]\nbuild-backend = \"poetry.core.masonry.api\"\n","toml","",[14,121,122,143,153,162,171,189,196,214,223,228,238,249],{"__ignoreMap":119},[123,124,127,131,135,137,140],"span",{"class":125,"line":126},"line",1,[123,128,130],{"class":129},"sVt8B","[",[123,132,134],{"class":133},"sScJk","tool",[123,136,53],{"class":129},[123,138,139],{"class":133},"poetry",[123,141,142],{"class":129},"]\n",[123,144,146,149],{"class":125,"line":145},2,[123,147,148],{"class":129},"name = ",[123,150,152],{"class":151},"sZZnC","\"mycli\"\n",[123,154,156,159],{"class":125,"line":155},3,[123,157,158],{"class":129},"version = ",[123,160,161],{"class":151},"\"0.1.0\"\n",[123,163,165,168],{"class":125,"line":164},4,[123,166,167],{"class":129},"description = ",[123,169,170],{"class":151},"\"A friendly command-line tool.\"\n",[123,172,174,177,180,183,186],{"class":125,"line":173},5,[123,175,176],{"class":129},"packages = [{ include = ",[123,178,179],{"class":151},"\"mycli\"",[123,181,182],{"class":129},", from = ",[123,184,185],{"class":151},"\"src\"",[123,187,188],{"class":129}," }]\n",[123,190,192],{"class":125,"line":191},6,[123,193,195],{"emptyLinePlaceholder":194},true,"\n",[123,197,199,201,203,205,207,209,212],{"class":125,"line":198},7,[123,200,130],{"class":129},[123,202,134],{"class":133},[123,204,53],{"class":129},[123,206,139],{"class":133},[123,208,53],{"class":129},[123,210,211],{"class":133},"scripts",[123,213,142],{"class":129},[123,215,217,220],{"class":125,"line":216},8,[123,218,219],{"class":129},"mycli = ",[123,221,222],{"class":151},"\"mycli.cli:app\"\n",[123,224,226],{"class":125,"line":225},9,[123,227,195],{"emptyLinePlaceholder":194},[123,229,231,233,236],{"class":125,"line":230},10,[123,232,130],{"class":129},[123,234,235],{"class":133},"build-system",[123,237,142],{"class":129},[123,239,241,244,247],{"class":125,"line":240},11,[123,242,243],{"class":129},"requires = [",[123,245,246],{"class":151},"\"poetry-core>=1.8\"",[123,248,142],{"class":129},[123,250,252,255],{"class":125,"line":251},12,[123,253,254],{"class":129},"build-backend = ",[123,256,257],{"class":151},"\"poetry.core.masonry.api\"\n",[10,259,260,261,264,265,269,270,273,274,277,278,280],{},"The value ",[14,262,263],{},"\"mycli.cli:app\""," is an ",[266,267,268],"em",{},"object reference",": the part before the colon is the module to import (",[14,271,272],{},"mycli.cli","), and the part after is the attribute to call (",[14,275,276],{},"app","). When someone runs ",[14,279,83],{},", the generated wrapper imports that module and calls that object with no arguments.",[10,282,283,284,287,288,290],{},"Here is the matching ",[14,285,286],{},"src\u002Fmycli\u002Fcli.py"," using Typer, whose ",[14,289,276],{}," object is directly callable:",[114,292,296],{"className":293,"code":294,"language":295,"meta":119,"style":119},"language-python shiki shiki-themes github-light github-dark","import typer\n\napp = typer.Typer(help=\"A friendly command-line tool.\")\n\n\n@app.command()\ndef hello(name: str = \"world\") -> None:\n    \"\"\"Greet someone.\"\"\"\n    typer.echo(f\"Hello, {name}!\")\n\n\nif __name__ == \"__main__\":\n    app()\n","python",[14,297,298,307,311,334,338,342,350,380,385,410,414,418,434],{"__ignoreMap":119},[123,299,300,304],{"class":125,"line":126},[123,301,303],{"class":302},"szBVR","import",[123,305,306],{"class":129}," typer\n",[123,308,309],{"class":125,"line":145},[123,310,195],{"emptyLinePlaceholder":194},[123,312,313,316,319,322,326,328,331],{"class":125,"line":155},[123,314,315],{"class":129},"app ",[123,317,318],{"class":302},"=",[123,320,321],{"class":129}," typer.Typer(",[123,323,325],{"class":324},"s4XuR","help",[123,327,318],{"class":302},[123,329,330],{"class":151},"\"A friendly command-line tool.\"",[123,332,333],{"class":129},")\n",[123,335,336],{"class":125,"line":164},[123,337,195],{"emptyLinePlaceholder":194},[123,339,340],{"class":125,"line":173},[123,341,195],{"emptyLinePlaceholder":194},[123,343,344,347],{"class":125,"line":191},[123,345,346],{"class":133},"@app.command",[123,348,349],{"class":129},"()\n",[123,351,352,355,358,361,365,368,371,374,377],{"class":125,"line":198},[123,353,354],{"class":302},"def",[123,356,357],{"class":133}," hello",[123,359,360],{"class":129},"(name: ",[123,362,364],{"class":363},"sj4cs","str",[123,366,367],{"class":302}," =",[123,369,370],{"class":151}," \"world\"",[123,372,373],{"class":129},") -> ",[123,375,376],{"class":363},"None",[123,378,379],{"class":129},":\n",[123,381,382],{"class":125,"line":216},[123,383,384],{"class":151},"    \"\"\"Greet someone.\"\"\"\n",[123,386,387,390,393,396,399,402,405,408],{"class":125,"line":225},[123,388,389],{"class":129},"    typer.echo(",[123,391,392],{"class":302},"f",[123,394,395],{"class":151},"\"Hello, ",[123,397,398],{"class":363},"{",[123,400,401],{"class":129},"name",[123,403,404],{"class":363},"}",[123,406,407],{"class":151},"!\"",[123,409,333],{"class":129},[123,411,412],{"class":125,"line":230},[123,413,195],{"emptyLinePlaceholder":194},[123,415,416],{"class":125,"line":240},[123,417,195],{"emptyLinePlaceholder":194},[123,419,420,423,426,429,432],{"class":125,"line":251},[123,421,422],{"class":302},"if",[123,424,425],{"class":363}," __name__",[123,427,428],{"class":302}," ==",[123,430,431],{"class":151}," \"__main__\"",[123,433,379],{"class":129},[123,435,437],{"class":125,"line":436},13,[123,438,439],{"class":129},"    app()\n",[10,441,442,443,446,447,450],{},"With Click the target is usually the ",[14,444,445],{},"@click.group()"," or ",[14,448,449],{},"@click.command()","-decorated function, which is also callable with no arguments:",[114,452,454],{"className":293,"code":453,"language":295,"meta":119,"style":119},"import click\n\n\n@click.group()\ndef app() -> None:\n    \"\"\"A friendly command-line tool.\"\"\"\n\n\n@app.command()\n@click.option(\"--name\", default=\"world\")\ndef hello(name: str) -> None:\n    click.echo(f\"Hello, {name}!\")\n",[14,455,456,463,467,471,478,492,497,501,505,511,535,551],{"__ignoreMap":119},[123,457,458,460],{"class":125,"line":126},[123,459,303],{"class":302},[123,461,462],{"class":129}," click\n",[123,464,465],{"class":125,"line":145},[123,466,195],{"emptyLinePlaceholder":194},[123,468,469],{"class":125,"line":155},[123,470,195],{"emptyLinePlaceholder":194},[123,472,473,476],{"class":125,"line":164},[123,474,475],{"class":133},"@click.group",[123,477,349],{"class":129},[123,479,480,482,485,488,490],{"class":125,"line":173},[123,481,354],{"class":302},[123,483,484],{"class":133}," app",[123,486,487],{"class":129},"() -> ",[123,489,376],{"class":363},[123,491,379],{"class":129},[123,493,494],{"class":125,"line":191},[123,495,496],{"class":151},"    \"\"\"A friendly command-line tool.\"\"\"\n",[123,498,499],{"class":125,"line":198},[123,500,195],{"emptyLinePlaceholder":194},[123,502,503],{"class":125,"line":216},[123,504,195],{"emptyLinePlaceholder":194},[123,506,507,509],{"class":125,"line":225},[123,508,346],{"class":133},[123,510,349],{"class":129},[123,512,513,516,519,522,525,528,530,533],{"class":125,"line":230},[123,514,515],{"class":133},"@click.option",[123,517,518],{"class":129},"(",[123,520,521],{"class":151},"\"--name\"",[123,523,524],{"class":129},", ",[123,526,527],{"class":324},"default",[123,529,318],{"class":302},[123,531,532],{"class":151},"\"world\"",[123,534,333],{"class":129},[123,536,537,539,541,543,545,547,549],{"class":125,"line":240},[123,538,354],{"class":302},[123,540,357],{"class":133},[123,542,360],{"class":129},[123,544,364],{"class":363},[123,546,373],{"class":129},[123,548,376],{"class":363},[123,550,379],{"class":129},[123,552,553,556,558,560,562,564,566,568],{"class":125,"line":251},[123,554,555],{"class":129},"    click.echo(",[123,557,392],{"class":302},[123,559,395],{"class":151},[123,561,398],{"class":363},[123,563,401],{"class":129},[123,565,404],{"class":363},[123,567,407],{"class":151},[123,569,333],{"class":129},[10,571,572,573,575,576,579,580,53],{},"Either way the scripts table points at ",[14,574,276],{},", and Poetry does the rest. The deeper rules for choosing a good target — why a thin ",[14,577,578],{},"main()"," wrapper beats calling framework internals — are in ",[581,582,584],"a",{"href":583},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","best practices for Python CLI entry points",[35,586,588,589],{"id":587},"the-pep-621-equivalent-projectscripts","The PEP 621 equivalent: ",[14,590,64],{},[10,592,593,594,597,598,600],{},"Since poetry-core 2.0, Poetry fully supports the standardized ",[14,595,596],{},"[project]"," metadata table, and console scripts move to ",[14,599,64],{},". This is the form to prefer in new projects because any PEP 517 build backend understands it — your entry points survive a future switch away from Poetry.",[114,602,604],{"className":116,"code":603,"language":118,"meta":119,"style":119},"[project]\nname = \"mycli\"\nversion = \"0.1.0\"\ndescription = \"A friendly command-line tool.\"\nrequires-python = \">=3.11\"\ndependencies = [\"typer>=0.12\"]\n\n[project.scripts]\nmycli = \"mycli.cli:app\"\n\n[tool.poetry]\npackages = [{ include = \"mycli\", from = \"src\" }]\n\n[build-system]\nrequires = [\"poetry-core>=2.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n",[14,605,606,615,621,627,633,641,651,655,667,673,677,689,701,705,714,724],{"__ignoreMap":119},[123,607,608,610,613],{"class":125,"line":126},[123,609,130],{"class":129},[123,611,612],{"class":133},"project",[123,614,142],{"class":129},[123,616,617,619],{"class":125,"line":145},[123,618,148],{"class":129},[123,620,152],{"class":151},[123,622,623,625],{"class":125,"line":155},[123,624,158],{"class":129},[123,626,161],{"class":151},[123,628,629,631],{"class":125,"line":164},[123,630,167],{"class":129},[123,632,170],{"class":151},[123,634,635,638],{"class":125,"line":173},[123,636,637],{"class":129},"requires-python = ",[123,639,640],{"class":151},"\">=3.11\"\n",[123,642,643,646,649],{"class":125,"line":191},[123,644,645],{"class":129},"dependencies = [",[123,647,648],{"class":151},"\"typer>=0.12\"",[123,650,142],{"class":129},[123,652,653],{"class":125,"line":198},[123,654,195],{"emptyLinePlaceholder":194},[123,656,657,659,661,663,665],{"class":125,"line":216},[123,658,130],{"class":129},[123,660,612],{"class":133},[123,662,53],{"class":129},[123,664,211],{"class":133},[123,666,142],{"class":129},[123,668,669,671],{"class":125,"line":225},[123,670,219],{"class":129},[123,672,222],{"class":151},[123,674,675],{"class":125,"line":230},[123,676,195],{"emptyLinePlaceholder":194},[123,678,679,681,683,685,687],{"class":125,"line":240},[123,680,130],{"class":129},[123,682,134],{"class":133},[123,684,53],{"class":129},[123,686,139],{"class":133},[123,688,142],{"class":129},[123,690,691,693,695,697,699],{"class":125,"line":251},[123,692,176],{"class":129},[123,694,179],{"class":151},[123,696,182],{"class":129},[123,698,185],{"class":151},[123,700,188],{"class":129},[123,702,703],{"class":125,"line":436},[123,704,195],{"emptyLinePlaceholder":194},[123,706,708,710,712],{"class":125,"line":707},14,[123,709,130],{"class":129},[123,711,235],{"class":133},[123,713,142],{"class":129},[123,715,717,719,722],{"class":125,"line":716},15,[123,718,243],{"class":129},[123,720,721],{"class":151},"\"poetry-core>=2.0\"",[123,723,142],{"class":129},[123,725,727,729],{"class":125,"line":726},16,[123,728,254],{"class":129},[123,730,257],{"class":151},[10,732,733,734,737,738,740,741,743,744,524,747,524,750,753,754,756,757,25,759,762,763,766,767,53],{},"The syntax inside the table is identical — ",[14,735,736],{},"command = \"module:callable\"",". What changes is the table name and portability: ",[14,739,48],{}," is only read by poetry-core, while ",[14,742,64],{}," is the interoperable standard that ",[14,745,746],{},"pip",[14,748,749],{},"uv",[14,751,752],{},"hatchling",", and every other modern tool reads. Note you cannot split metadata arbitrarily: if you declare ",[14,755,596],{},", dependencies and scripts belong under ",[14,758,596],{},[14,760,761],{},"[tool.poetry]"," shrinks to Poetry-specific settings like ",[14,764,765],{},"packages",". This is the same convergence discussed in ",[581,768,770],{"href":769},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools\u002F","uv init vs poetry init for CLI tools",[35,772,774,776],{"id":773},"poetry-install-makes-the-command-available",[14,775,28],{}," makes the command available",[10,778,779,780,782],{},"Declaring the script is not enough — a build step has to generate the wrapper executable. ",[14,781,28],{}," does that as part of installing your own project into its environment:",[110,784],{"name":785},"poetry-install-shim",[114,787,791],{"className":788,"code":789,"language":790,"meta":119,"style":119},"language-bash shiki shiki-themes github-light github-dark","$ poetry install\nInstalling dependencies from lock file\nInstalling the current project: mycli (0.1.0)\n\n$ poetry run mycli hello --name Ada\nHello, Ada!\n","bash",[14,792,793,804,821,840,844,863],{"__ignoreMap":119},[123,794,795,798,801],{"class":125,"line":126},[123,796,797],{"class":133},"$",[123,799,800],{"class":151}," poetry",[123,802,803],{"class":151}," install\n",[123,805,806,809,812,815,818],{"class":125,"line":145},[123,807,808],{"class":133},"Installing",[123,810,811],{"class":151}," dependencies",[123,813,814],{"class":151}," from",[123,816,817],{"class":151}," lock",[123,819,820],{"class":151}," file\n",[123,822,823,825,828,831,834,837],{"class":125,"line":155},[123,824,808],{"class":133},[123,826,827],{"class":151}," the",[123,829,830],{"class":151}," current",[123,832,833],{"class":151}," project:",[123,835,836],{"class":151}," mycli",[123,838,839],{"class":129}," (0.1.0)\n",[123,841,842],{"class":125,"line":164},[123,843,195],{"emptyLinePlaceholder":194},[123,845,846,848,850,853,855,857,860],{"class":125,"line":173},[123,847,797],{"class":133},[123,849,800],{"class":151},[123,851,852],{"class":151}," run",[123,854,836],{"class":151},[123,856,357],{"class":151},[123,858,859],{"class":363}," --name",[123,861,862],{"class":151}," Ada\n",[123,864,865,868],{"class":125,"line":191},[123,866,867],{"class":133},"Hello,",[123,869,870],{"class":151}," Ada!\n",[10,872,873,875,876,879,880,883,884,887,888,891,892,895,896,899,900,902],{},[14,874,28],{}," performs an ",[266,877,878],{},"editable"," install of your project by default (equivalent to ",[14,881,882],{},"pip install -e .","): it links your source directory into the environment rather than copying it, and it writes the console-script wrapper into the environment's ",[14,885,886],{},"bin\u002F"," (or ",[14,889,890],{},"Scripts\\"," on Windows). Because the install is editable, edits to ",[14,893,894],{},"cli.py"," take effect immediately — no reinstall needed. What ",[266,897,898],{},"does"," require a re-run of ",[14,901,28],{}," is changing the scripts table itself, since the wrapper is generated from that metadata.",[35,904,906,907,910],{"id":905},"testing-the-entry-point-with-poetry-run-and-an-activated-shell","Testing the entry point with ",[14,908,909],{},"poetry run"," and an activated shell",[10,912,913],{},"You have two ways to invoke the command inside the managed environment without publishing anything.",[10,915,916,917,919],{},"The quickest is ",[14,918,909],{},", which runs a single command in the environment:",[114,921,923],{"className":788,"code":922,"language":790,"meta":119,"style":119},"$ poetry run mycli --help\n$ poetry run mycli hello --name world\n",[14,924,925,938],{"__ignoreMap":119},[123,926,927,929,931,933,935],{"class":125,"line":126},[123,928,797],{"class":133},[123,930,800],{"class":151},[123,932,852],{"class":151},[123,934,836],{"class":151},[123,936,937],{"class":363}," --help\n",[123,939,940,942,944,946,948,950,952],{"class":125,"line":145},[123,941,797],{"class":133},[123,943,800],{"class":151},[123,945,852],{"class":151},[123,947,836],{"class":151},[123,949,357],{"class":151},[123,951,859],{"class":363},[123,953,954],{"class":151}," world\n",[10,956,957,958,961,962,965],{},"If you are iterating and want the command available across many invocations, activate the environment instead. On Poetry 2.x this is the ",[14,959,960],{},"env activate"," command (older versions used ",[14,963,964],{},"poetry shell","):",[114,967,969],{"className":788,"code":968,"language":790,"meta":119,"style":119},"$ eval $(poetry env activate)\n(mycli-py3.11) $ mycli hello\nHello, world!\n(mycli-py3.11) $ which mycli\n\u002Fhome\u002Fyou\u002F.cache\u002Fpypoetry\u002Fvirtualenvs\u002Fmycli-py3.11\u002Fbin\u002Fmycli\n",[14,970,971,991,1008,1015,1031],{"__ignoreMap":119},[123,972,973,975,978,981,983,986,989],{"class":125,"line":126},[123,974,797],{"class":133},[123,976,977],{"class":151}," eval",[123,979,980],{"class":129}," $(",[123,982,139],{"class":133},[123,984,985],{"class":151}," env",[123,987,988],{"class":151}," activate",[123,990,333],{"class":129},[123,992,993,995,998,1001,1003,1005],{"class":125,"line":145},[123,994,518],{"class":129},[123,996,997],{"class":133},"mycli-py3.11",[123,999,1000],{"class":129},") ",[123,1002,797],{"class":133},[123,1004,836],{"class":151},[123,1006,1007],{"class":151}," hello\n",[123,1009,1010,1012],{"class":125,"line":155},[123,1011,867],{"class":133},[123,1013,1014],{"class":151}," world!\n",[123,1016,1017,1019,1021,1023,1025,1028],{"class":125,"line":164},[123,1018,518],{"class":129},[123,1020,997],{"class":133},[123,1022,1000],{"class":129},[123,1024,797],{"class":133},[123,1026,1027],{"class":151}," which",[123,1029,1030],{"class":151}," mycli\n",[123,1032,1033],{"class":125,"line":173},[123,1034,1035],{"class":133},"\u002Fhome\u002Fyou\u002F.cache\u002Fpypoetry\u002Fvirtualenvs\u002Fmycli-py3.11\u002Fbin\u002Fmycli\n",[10,1037,1038,1041,1042,1044,1045,1048,1049,1051,1052,1054,1055,1058],{},[14,1039,1040],{},"which mycli"," pointing inside the Poetry virtualenv is the proof that the wrapper was generated and put on ",[14,1043,32],{},". If the command is ",[266,1046,1047],{},"not"," found after ",[14,1050,28],{},", the usual causes are a typo in the object reference, a package that was not actually included (check the ",[14,1053,765],{}," setting), or forgetting that the current project must be installed — ",[14,1056,1057],{},"poetry install --no-root"," deliberately skips your package and therefore its scripts.",[35,1060,1062],{"id":1061},"verifying-the-object-reference-resolves","Verifying the object reference resolves",[10,1064,1065],{},"A subtle failure mode is a scripts table that builds fine but whose target cannot be imported at runtime — a renamed module, a callable that moved, or a typo. Catch it early by importing the reference yourself:",[110,1067],{"name":1068},"poetry-entry-point-check",[114,1070,1072],{"className":788,"code":1071,"language":790,"meta":119,"style":119},"# Does the module import and does the callable exist?\n$ poetry run python -c \"from mycli.cli import app; print(app)\"\n\u003Ctyper.main.Typer object at 0x...>\n",[14,1073,1074,1080,1097],{"__ignoreMap":119},[123,1075,1076],{"class":125,"line":126},[123,1077,1079],{"class":1078},"sJ8bj","# Does the module import and does the callable exist?\n",[123,1081,1082,1084,1086,1088,1091,1094],{"class":125,"line":145},[123,1083,797],{"class":133},[123,1085,800],{"class":151},[123,1087,852],{"class":151},[123,1089,1090],{"class":151}," python",[123,1092,1093],{"class":363}," -c",[123,1095,1096],{"class":151}," \"from mycli.cli import app; print(app)\"\n",[123,1098,1099,1102,1105],{"class":125,"line":155},[123,1100,1101],{"class":302},"\u003C",[123,1103,1104],{"class":129},"typer.main.Typer object at 0x...",[123,1106,1107],{"class":302},">\n",[10,1109,1110,1111,446,1114,1117,1118,1120,1121,53],{},"If that line raises ",[14,1112,1113],{},"ModuleNotFoundError",[14,1115,1116],{},"ImportError",", your ",[14,1119,83],{}," command would fail with the same error, just wrapped in console-script boilerplate. This one-liner belongs in a smoke test in CI so a broken entry point never reaches users. It pairs well with the checks in ",[581,1122,1124],{"href":1123},"\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",[10,1126,1127,1128,1131,1132,1135],{},"For an even stricter check, build the wheel and inspect the metadata that will actually ship. The entry point is written into the distribution's ",[14,1129,1130],{},"entry_points.txt",", and reading it back confirms the exact command-to-callable mapping a user's ",[14,1133,1134],{},"pip install"," will register:",[114,1137,1139],{"className":788,"code":1138,"language":790,"meta":119,"style":119},"$ poetry build\n$ python -c \"\nfrom importlib.metadata import distribution\nd = distribution('mycli')\nfor ep in d.entry_points.select(group='console_scripts'):\n    print(ep.name, '->', ep.value)\n\"\nmycli -> mycli.cli:app\n",[14,1140,1141,1150,1161,1166,1171,1176,1181,1186],{"__ignoreMap":119},[123,1142,1143,1145,1147],{"class":125,"line":126},[123,1144,797],{"class":133},[123,1146,800],{"class":151},[123,1148,1149],{"class":151}," build\n",[123,1151,1152,1154,1156,1158],{"class":125,"line":145},[123,1153,797],{"class":133},[123,1155,1090],{"class":151},[123,1157,1093],{"class":363},[123,1159,1160],{"class":151}," \"\n",[123,1162,1163],{"class":125,"line":155},[123,1164,1165],{"class":151},"from importlib.metadata import distribution\n",[123,1167,1168],{"class":125,"line":164},[123,1169,1170],{"class":151},"d = distribution('mycli')\n",[123,1172,1173],{"class":125,"line":173},[123,1174,1175],{"class":151},"for ep in d.entry_points.select(group='console_scripts'):\n",[123,1177,1178],{"class":125,"line":191},[123,1179,1180],{"class":151},"    print(ep.name, '->', ep.value)\n",[123,1182,1183],{"class":125,"line":198},[123,1184,1185],{"class":151},"\"\n",[123,1187,1188,1190,1193,1196],{"class":125,"line":216},[123,1189,83],{"class":133},[123,1191,1192],{"class":129}," -",[123,1194,1195],{"class":302},">",[123,1197,1198],{"class":151}," mycli.cli:app\n",[10,1200,1201,1202,1205,1206,1209,1210,1212],{},"Seeing ",[14,1203,1204],{},"mycli -> mycli.cli:app"," printed from the ",[266,1207,1208],{},"installed"," metadata — not just the source ",[14,1211,24],{}," — is the strongest confirmation you can get short of publishing. It is also how tools like pipx and uv discover which commands to expose when someone installs your CLI globally.",[35,1214,1216],{"id":1215},"scripts-vs-plugins-two-different-mechanisms","Scripts vs plugins: two different mechanisms",[10,1218,1219,1221,1222,1224,1225,1228,1229,1232,1233,1235,1236,1239,1240,1243,1244,1248,1249,1252],{},[14,1220,48],{}," \u002F ",[14,1223,64],{}," register ",[266,1226,1227],{},"console scripts"," — commands your user runs. They are unrelated to Poetry's own ",[266,1230,1231],{},"plugins",", which extend the ",[14,1234,139],{}," tool itself and are declared under ",[14,1237,1238],{},"[tool.poetry.plugins]"," against a named entry-point group. If you are building an extensible CLI where third parties register subcommands into ",[266,1241,1242],{},"your"," tool, that is the same underlying entry-point-group mechanism, covered in ",[581,1245,1247],{"href":1246},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002F","plugin architectures for extensible CLIs",". Keep the two mental models separate: the scripts table ships your command; entry-point ",[266,1250,1251],{},"groups"," let other packages plug into a host application.",[35,1254,1256],{"id":1255},"production-notes","Production notes",[40,1258,1259,1279,1291,1300,1313,1329],{},[43,1260,1261,1264,1265,1268,1269,1271,1272,1274,1275,1278],{},[58,1262,1263],{},"The target must accept zero arguments."," ",[14,1266,1267],{},"mycli = \"mycli.cli:main\""," calls ",[14,1270,578],{}," with no args. Frameworks parse ",[14,1273,94],{}," internally, so a ",[14,1276,1277],{},"main(argv)"," signature will crash the wrapper.",[43,1280,1281,1287,1288,1290],{},[58,1282,1283,1284,1286],{},"Prefer ",[14,1285,64],{}," for new projects."," It is portable across build backends; reserve ",[14,1289,48],{}," for repos still on poetry-core \u003C 2.0.",[43,1292,1293,1299],{},[58,1294,1295,1296,1298],{},"Re-run ",[14,1297,28],{}," after editing the scripts table."," Source edits are picked up automatically (editable install), but wrapper regeneration is not.",[43,1301,1302,1305,1306,524,1309,1312],{},[58,1303,1304],{},"Multiple commands are fine."," Add more lines to the table (",[14,1307,1308],{},"mycli = ...",[14,1310,1311],{},"mycli-admin = ...",") to ship several executables from one package.",[43,1314,1315,1318,1319,1321,1322,1325,1326,1328],{},[58,1316,1317],{},"Cross-platform wrappers differ."," Poetry writes a ",[14,1320,886],{}," script on Unix and a ",[14,1323,1324],{},".exe"," shim in ",[14,1327,890],{}," on Windows; both resolve the same object reference, so no per-OS code is needed.",[43,1330,1331,1334,1335,1338,1339,1342],{},[58,1332,1333],{},"Don't hardcode the entry point in tests."," Test the callable directly (",[14,1336,1337],{},"from mycli.cli import app",") using Click's ",[14,1340,1341],{},"CliRunner"," or Typer's test runner rather than shelling out to the installed command.",[35,1344,1346],{"id":1345},"frequently-asked-questions","Frequently asked questions",[1348,1349,1351,1352,1355,1356,1359],"h3",{"id":1350},"what-is-the-difference-between-toolpoetryscripts-and-projectscripts","What is the difference between ",[123,1353,1354],{},"tool.poetry.scripts"," and ",[123,1357,1358],{},"project.scripts","?",[10,1361,1362,1363,1365,1366,1368],{},"They express the same thing in two dialects. ",[123,1364,1358],{}," is the standardised PEP 621 table that every backend understands; ",[123,1367,1354],{}," is Poetry's older equivalent. Recent Poetry supports the standard table, and using it is the better default because the file then means the same thing to pip, uv, build and anything else.",[1348,1370,1372],{"id":1371},"why-is-my-command-not-found-after-poetry-install","Why is my command not found after poetry install?",[10,1374,1375],{},"Because the shim lives inside the project's virtual environment, not on your global PATH. Either prefix the command with poetry run, or activate the environment first. If it is missing even there, the target named in the scripts table does not resolve — check it by importing it directly with poetry run python -c.",[1348,1377,1379],{"id":1378},"do-i-need-to-re-install-after-changing-the-scripts-table","Do I need to re-install after changing the scripts table?",[10,1381,1382],{},"Yes. The launcher is generated during installation, so a renamed command or a changed target only takes effect on the next poetry install. Editing the table and wondering why nothing changed is the single most common confusion here.",[1348,1384,1386],{"id":1385},"can-one-project-ship-several-commands","Can one project ship several commands?",[10,1388,1389],{},"Yes — add one line per command to the table, each pointing at its own callable. That is how tools ship a main command plus a small companion like a doctor or a migration helper. Keep the number small: each name is a permanent claim on the user's PATH.",[1348,1391,1393],{"id":1392},"are-scripts-and-plugin-entry-points-the-same-mechanism","Are scripts and plugin entry points the same mechanism?",[10,1395,1396],{},"They use the same underlying entry-point metadata but different groups. console_scripts creates executables on PATH; a custom group such as mytool.plugins is read at runtime by your own code to discover extensions. Poetry writes both from pyproject.toml, and confusing the two is why a plugin sometimes shows up as a stray command.",[35,1398,1400],{"id":1399},"related","Related",[40,1402,1403,1410,1416,1421],{},[43,1404,1405,1409],{},[581,1406,1408],{"href":1407},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002F","Poetry workflows for CLI development"," — the parent guide covering the full Poetry lifecycle for CLI projects.",[43,1411,1412,1415],{},[581,1413,1414],{"href":583},"Best practices for Python CLI entry points"," — how to design the callable your scripts table points at.",[43,1417,1418,1420],{},[581,1419,770],{"href":769}," — the same scripts table seen from uv's side.",[43,1422,1423,1426],{},[581,1424,1425],{"href":1246},"Plugin architectures for extensible CLIs"," — entry-point groups for letting third parties extend your CLI.",[1428,1429,1430],"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 .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 .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}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":119,"searchDepth":145,"depth":145,"links":1432},[1433,1434,1435,1437,1439,1441,1442,1443,1444,1452],{"id":37,"depth":145,"text":38},{"id":101,"depth":145,"text":102},{"id":587,"depth":145,"text":1436},"The PEP 621 equivalent: [project.scripts]",{"id":773,"depth":145,"text":1438},"poetry install makes the command available",{"id":905,"depth":145,"text":1440},"Testing the entry point with poetry run and an activated shell",{"id":1061,"depth":145,"text":1062},{"id":1215,"depth":145,"text":1216},{"id":1255,"depth":145,"text":1256},{"id":1345,"depth":145,"text":1346,"children":1445},[1446,1448,1449,1450,1451],{"id":1350,"depth":155,"text":1447},"What is the difference between tool.poetry.scripts and project.scripts?",{"id":1371,"depth":155,"text":1372},{"id":1378,"depth":155,"text":1379},{"id":1385,"depth":155,"text":1386},{"id":1392,"depth":155,"text":1393},{"id":1399,"depth":145,"text":1400},"2026-07-05","Define console entry points with the Poetry scripts table, expose your CLI command on install, and test it with poetry run and editable installs before shipping.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis",{"title":5,"description":1454},"project-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis\u002Findex",[139,1463,1464,1465,1466],"entry-points","cli","packaging","pyproject","QIf7iSIkMgsVEHp4E8e4loaaEp-Q2CgeGB6LQh2-5VY",[1469,1472,1475,1478,1481,1484,1487,1490,1493,1496,1499,1502,1505,1508,1511,1514,1517,1520,1523,1526,1529,1532,1535,1538,1541,1544,1547,1550,1553,1556,1559,1562,1565,1568,1571,1574,1577,1580,1583,1586,1589,1592,1595,1598,1600,1603,1606,1609,1612,1615,1618,1621,1624,1627,1630,1633,1636,1639,1642,1645,1648,1651,1654,1657,1660,1663,1666,1669,1672,1675,1678,1681,1684,1687,1690,1691,1694,1697,1700,1702,1705,1708,1711],{"path":1470,"title":1471},"\u002Fabout","About Python CLI Toolcraft",{"path":1473,"title":1474},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1476,"title":1477},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1479,"title":1480},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1482,"title":1483},"\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":1485,"title":1486},"\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":1488,"title":1489},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1491,"title":1492},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1494,"title":1495},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1497,"title":1498},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1500,"title":1501},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1503,"title":1504},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1506,"title":1507},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1509,"title":1510},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1512,"title":1513},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1515,"title":1516},"\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":1518,"title":1519},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1521,"title":1522},"\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":1524,"title":1525},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1527,"title":1528},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1530,"title":1531},"\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":1533,"title":1534},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1536,"title":1537},"\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":1539,"title":1540},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1542,"title":1543},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1545,"title":1546},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1548,"title":1549},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1551,"title":1552},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1554,"title":1555},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1557,"title":1558},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1560,"title":1561},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1563,"title":1564},"\u002F","Python CLI Toolcraft",{"path":1566,"title":1567},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1569,"title":1570},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1572,"title":1573},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1575,"title":1576},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1578,"title":1579},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1581,"title":1582},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1584,"title":1585},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1587,"title":1588},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1590,"title":1591},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1593,"title":1594},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1596,"title":1597},"\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":1599,"title":1414},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points",{"path":1601,"title":1602},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1604,"title":1605},"\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":1607,"title":1608},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1610,"title":1611},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1613,"title":1614},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1616,"title":1617},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1619,"title":1620},"\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":1622,"title":1623},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1625,"title":1626},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1628,"title":1629},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1631,"title":1632},"\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":1634,"title":1635},"\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":1637,"title":1638},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1640,"title":1641},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1643,"title":1644},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1646,"title":1647},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1649,"title":1650},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1652,"title":1653},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1655,"title":1656},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1658,"title":1659},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1661,"title":1662},"\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":1664,"title":1665},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1667,"title":1668},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"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\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1679,"title":1680},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1682,"title":1683},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1685,"title":1686},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1688,"title":1689},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1459,"title":5},{"path":1692,"title":1693},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1695,"title":1696},"\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":1698,"title":1699},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1701,"title":770},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools",{"path":1703,"title":1704},"\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":1706,"title":1707},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1709,"title":1710},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1712,"title":1713},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690035]