[{"data":1,"prerenderedAt":1546},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis\u002F":3,"content-directory":1396},{"id":4,"title":5,"body":6,"date":1381,"description":1382,"difficulty":1383,"draft":1384,"extension":1385,"meta":1386,"navigation":190,"path":1387,"seo":1388,"stem":1389,"tags":1390,"updated":1381,"__hash__":1395},"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":1367},"minimark",[9,34,39,99,103,109,254,277,287,436,447,566,581,587,597,727,767,773,779,864,896,904,907,913,948,959,1029,1052,1056,1059,1098,1115,1126,1189,1203,1207,1243,1247,1333,1337,1363],[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,116],"pre",{"className":112,"code":113,"language":114,"meta":115,"style":115},"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,117,118,139,149,158,167,185,192,210,219,224,234,245],{"__ignoreMap":115},[119,120,123,127,131,133,136],"span",{"class":121,"line":122},"line",1,[119,124,126],{"class":125},"sVt8B","[",[119,128,130],{"class":129},"sScJk","tool",[119,132,53],{"class":125},[119,134,135],{"class":129},"poetry",[119,137,138],{"class":125},"]\n",[119,140,142,145],{"class":121,"line":141},2,[119,143,144],{"class":125},"name = ",[119,146,148],{"class":147},"sZZnC","\"mycli\"\n",[119,150,152,155],{"class":121,"line":151},3,[119,153,154],{"class":125},"version = ",[119,156,157],{"class":147},"\"0.1.0\"\n",[119,159,161,164],{"class":121,"line":160},4,[119,162,163],{"class":125},"description = ",[119,165,166],{"class":147},"\"A friendly command-line tool.\"\n",[119,168,170,173,176,179,182],{"class":121,"line":169},5,[119,171,172],{"class":125},"packages = [{ include = ",[119,174,175],{"class":147},"\"mycli\"",[119,177,178],{"class":125},", from = ",[119,180,181],{"class":147},"\"src\"",[119,183,184],{"class":125}," }]\n",[119,186,188],{"class":121,"line":187},6,[119,189,191],{"emptyLinePlaceholder":190},true,"\n",[119,193,195,197,199,201,203,205,208],{"class":121,"line":194},7,[119,196,126],{"class":125},[119,198,130],{"class":129},[119,200,53],{"class":125},[119,202,135],{"class":129},[119,204,53],{"class":125},[119,206,207],{"class":129},"scripts",[119,209,138],{"class":125},[119,211,213,216],{"class":121,"line":212},8,[119,214,215],{"class":125},"mycli = ",[119,217,218],{"class":147},"\"mycli.cli:app\"\n",[119,220,222],{"class":121,"line":221},9,[119,223,191],{"emptyLinePlaceholder":190},[119,225,227,229,232],{"class":121,"line":226},10,[119,228,126],{"class":125},[119,230,231],{"class":129},"build-system",[119,233,138],{"class":125},[119,235,237,240,243],{"class":121,"line":236},11,[119,238,239],{"class":125},"requires = [",[119,241,242],{"class":147},"\"poetry-core>=1.8\"",[119,244,138],{"class":125},[119,246,248,251],{"class":121,"line":247},12,[119,249,250],{"class":125},"build-backend = ",[119,252,253],{"class":147},"\"poetry.core.masonry.api\"\n",[10,255,256,257,260,261,265,266,269,270,273,274,276],{},"The value ",[14,258,259],{},"\"mycli.cli:app\""," is an ",[262,263,264],"em",{},"object reference",": the part before the colon is the module to import (",[14,267,268],{},"mycli.cli","), and the part after is the attribute to call (",[14,271,272],{},"app","). When someone runs ",[14,275,83],{},", the generated wrapper imports that module and calls that object with no arguments.",[10,278,279,280,283,284,286],{},"Here is the matching ",[14,281,282],{},"src\u002Fmycli\u002Fcli.py"," using Typer, whose ",[14,285,272],{}," object is directly callable:",[110,288,292],{"className":289,"code":290,"language":291,"meta":115,"style":115},"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,293,294,303,307,330,334,338,346,376,381,406,410,414,430],{"__ignoreMap":115},[119,295,296,300],{"class":121,"line":122},[119,297,299],{"class":298},"szBVR","import",[119,301,302],{"class":125}," typer\n",[119,304,305],{"class":121,"line":141},[119,306,191],{"emptyLinePlaceholder":190},[119,308,309,312,315,318,322,324,327],{"class":121,"line":151},[119,310,311],{"class":125},"app ",[119,313,314],{"class":298},"=",[119,316,317],{"class":125}," typer.Typer(",[119,319,321],{"class":320},"s4XuR","help",[119,323,314],{"class":298},[119,325,326],{"class":147},"\"A friendly command-line tool.\"",[119,328,329],{"class":125},")\n",[119,331,332],{"class":121,"line":160},[119,333,191],{"emptyLinePlaceholder":190},[119,335,336],{"class":121,"line":169},[119,337,191],{"emptyLinePlaceholder":190},[119,339,340,343],{"class":121,"line":187},[119,341,342],{"class":129},"@app.command",[119,344,345],{"class":125},"()\n",[119,347,348,351,354,357,361,364,367,370,373],{"class":121,"line":194},[119,349,350],{"class":298},"def",[119,352,353],{"class":129}," hello",[119,355,356],{"class":125},"(name: ",[119,358,360],{"class":359},"sj4cs","str",[119,362,363],{"class":298}," =",[119,365,366],{"class":147}," \"world\"",[119,368,369],{"class":125},") -> ",[119,371,372],{"class":359},"None",[119,374,375],{"class":125},":\n",[119,377,378],{"class":121,"line":212},[119,379,380],{"class":147},"    \"\"\"Greet someone.\"\"\"\n",[119,382,383,386,389,392,395,398,401,404],{"class":121,"line":221},[119,384,385],{"class":125},"    typer.echo(",[119,387,388],{"class":298},"f",[119,390,391],{"class":147},"\"Hello, ",[119,393,394],{"class":359},"{",[119,396,397],{"class":125},"name",[119,399,400],{"class":359},"}",[119,402,403],{"class":147},"!\"",[119,405,329],{"class":125},[119,407,408],{"class":121,"line":226},[119,409,191],{"emptyLinePlaceholder":190},[119,411,412],{"class":121,"line":236},[119,413,191],{"emptyLinePlaceholder":190},[119,415,416,419,422,425,428],{"class":121,"line":247},[119,417,418],{"class":298},"if",[119,420,421],{"class":359}," __name__",[119,423,424],{"class":298}," ==",[119,426,427],{"class":147}," \"__main__\"",[119,429,375],{"class":125},[119,431,433],{"class":121,"line":432},13,[119,434,435],{"class":125},"    app()\n",[10,437,438,439,442,443,446],{},"With Click the target is usually the ",[14,440,441],{},"@click.group()"," or ",[14,444,445],{},"@click.command()","-decorated function, which is also callable with no arguments:",[110,448,450],{"className":289,"code":449,"language":291,"meta":115,"style":115},"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,451,452,459,463,467,474,488,493,497,501,507,531,547],{"__ignoreMap":115},[119,453,454,456],{"class":121,"line":122},[119,455,299],{"class":298},[119,457,458],{"class":125}," click\n",[119,460,461],{"class":121,"line":141},[119,462,191],{"emptyLinePlaceholder":190},[119,464,465],{"class":121,"line":151},[119,466,191],{"emptyLinePlaceholder":190},[119,468,469,472],{"class":121,"line":160},[119,470,471],{"class":129},"@click.group",[119,473,345],{"class":125},[119,475,476,478,481,484,486],{"class":121,"line":169},[119,477,350],{"class":298},[119,479,480],{"class":129}," app",[119,482,483],{"class":125},"() -> ",[119,485,372],{"class":359},[119,487,375],{"class":125},[119,489,490],{"class":121,"line":187},[119,491,492],{"class":147},"    \"\"\"A friendly command-line tool.\"\"\"\n",[119,494,495],{"class":121,"line":194},[119,496,191],{"emptyLinePlaceholder":190},[119,498,499],{"class":121,"line":212},[119,500,191],{"emptyLinePlaceholder":190},[119,502,503,505],{"class":121,"line":221},[119,504,342],{"class":129},[119,506,345],{"class":125},[119,508,509,512,515,518,521,524,526,529],{"class":121,"line":226},[119,510,511],{"class":129},"@click.option",[119,513,514],{"class":125},"(",[119,516,517],{"class":147},"\"--name\"",[119,519,520],{"class":125},", ",[119,522,523],{"class":320},"default",[119,525,314],{"class":298},[119,527,528],{"class":147},"\"world\"",[119,530,329],{"class":125},[119,532,533,535,537,539,541,543,545],{"class":121,"line":236},[119,534,350],{"class":298},[119,536,353],{"class":129},[119,538,356],{"class":125},[119,540,360],{"class":359},[119,542,369],{"class":125},[119,544,372],{"class":359},[119,546,375],{"class":125},[119,548,549,552,554,556,558,560,562,564],{"class":121,"line":247},[119,550,551],{"class":125},"    click.echo(",[119,553,388],{"class":298},[119,555,391],{"class":147},[119,557,394],{"class":359},[119,559,397],{"class":125},[119,561,400],{"class":359},[119,563,403],{"class":147},[119,565,329],{"class":125},[10,567,568,569,571,572,575,576,53],{},"Either way the scripts table points at ",[14,570,272],{},", and Poetry does the rest. The deeper rules for choosing a good target — why a thin ",[14,573,574],{},"main()"," wrapper beats calling framework internals — are in ",[577,578,580],"a",{"href":579},"\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,582,584,585],{"id":583},"the-pep-621-equivalent-projectscripts","The PEP 621 equivalent: ",[14,586,64],{},[10,588,589,590,593,594,596],{},"Since poetry-core 2.0, Poetry fully supports the standardized ",[14,591,592],{},"[project]"," metadata table, and console scripts move to ",[14,595,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.",[110,598,600],{"className":112,"code":599,"language":114,"meta":115,"style":115},"[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,601,602,611,617,623,629,637,647,651,663,669,673,685,697,701,710,720],{"__ignoreMap":115},[119,603,604,606,609],{"class":121,"line":122},[119,605,126],{"class":125},[119,607,608],{"class":129},"project",[119,610,138],{"class":125},[119,612,613,615],{"class":121,"line":141},[119,614,144],{"class":125},[119,616,148],{"class":147},[119,618,619,621],{"class":121,"line":151},[119,620,154],{"class":125},[119,622,157],{"class":147},[119,624,625,627],{"class":121,"line":160},[119,626,163],{"class":125},[119,628,166],{"class":147},[119,630,631,634],{"class":121,"line":169},[119,632,633],{"class":125},"requires-python = ",[119,635,636],{"class":147},"\">=3.11\"\n",[119,638,639,642,645],{"class":121,"line":187},[119,640,641],{"class":125},"dependencies = [",[119,643,644],{"class":147},"\"typer>=0.12\"",[119,646,138],{"class":125},[119,648,649],{"class":121,"line":194},[119,650,191],{"emptyLinePlaceholder":190},[119,652,653,655,657,659,661],{"class":121,"line":212},[119,654,126],{"class":125},[119,656,608],{"class":129},[119,658,53],{"class":125},[119,660,207],{"class":129},[119,662,138],{"class":125},[119,664,665,667],{"class":121,"line":221},[119,666,215],{"class":125},[119,668,218],{"class":147},[119,670,671],{"class":121,"line":226},[119,672,191],{"emptyLinePlaceholder":190},[119,674,675,677,679,681,683],{"class":121,"line":236},[119,676,126],{"class":125},[119,678,130],{"class":129},[119,680,53],{"class":125},[119,682,135],{"class":129},[119,684,138],{"class":125},[119,686,687,689,691,693,695],{"class":121,"line":247},[119,688,172],{"class":125},[119,690,175],{"class":147},[119,692,178],{"class":125},[119,694,181],{"class":147},[119,696,184],{"class":125},[119,698,699],{"class":121,"line":432},[119,700,191],{"emptyLinePlaceholder":190},[119,702,704,706,708],{"class":121,"line":703},14,[119,705,126],{"class":125},[119,707,231],{"class":129},[119,709,138],{"class":125},[119,711,713,715,718],{"class":121,"line":712},15,[119,714,239],{"class":125},[119,716,717],{"class":147},"\"poetry-core>=2.0\"",[119,719,138],{"class":125},[119,721,723,725],{"class":121,"line":722},16,[119,724,250],{"class":125},[119,726,253],{"class":147},[10,728,729,730,733,734,736,737,739,740,520,743,520,746,749,750,752,753,25,755,758,759,762,763,53],{},"The syntax inside the table is identical — ",[14,731,732],{},"command = \"module:callable\"",". What changes is the table name and portability: ",[14,735,48],{}," is only read by poetry-core, while ",[14,738,64],{}," is the interoperable standard that ",[14,741,742],{},"pip",[14,744,745],{},"uv",[14,747,748],{},"hatchling",", and every other modern tool reads. Note you cannot split metadata arbitrarily: if you declare ",[14,751,592],{},", dependencies and scripts belong under ",[14,754,592],{},[14,756,757],{},"[tool.poetry]"," shrinks to Poetry-specific settings like ",[14,760,761],{},"packages",". This is the same convergence discussed in ",[577,764,766],{"href":765},"\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,768,770,772],{"id":769},"poetry-install-makes-the-command-available",[14,771,28],{}," makes the command available",[10,774,775,776,778],{},"Declaring the script is not enough — a build step has to generate the wrapper executable. ",[14,777,28],{}," does that as part of installing your own project into its environment:",[110,780,784],{"className":781,"code":782,"language":783,"meta":115,"style":115},"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,785,786,797,814,833,837,856],{"__ignoreMap":115},[119,787,788,791,794],{"class":121,"line":122},[119,789,790],{"class":129},"$",[119,792,793],{"class":147}," poetry",[119,795,796],{"class":147}," install\n",[119,798,799,802,805,808,811],{"class":121,"line":141},[119,800,801],{"class":129},"Installing",[119,803,804],{"class":147}," dependencies",[119,806,807],{"class":147}," from",[119,809,810],{"class":147}," lock",[119,812,813],{"class":147}," file\n",[119,815,816,818,821,824,827,830],{"class":121,"line":151},[119,817,801],{"class":129},[119,819,820],{"class":147}," the",[119,822,823],{"class":147}," current",[119,825,826],{"class":147}," project:",[119,828,829],{"class":147}," mycli",[119,831,832],{"class":125}," (0.1.0)\n",[119,834,835],{"class":121,"line":160},[119,836,191],{"emptyLinePlaceholder":190},[119,838,839,841,843,846,848,850,853],{"class":121,"line":169},[119,840,790],{"class":129},[119,842,793],{"class":147},[119,844,845],{"class":147}," run",[119,847,829],{"class":147},[119,849,353],{"class":147},[119,851,852],{"class":359}," --name",[119,854,855],{"class":147}," Ada\n",[119,857,858,861],{"class":121,"line":187},[119,859,860],{"class":129},"Hello,",[119,862,863],{"class":147}," Ada!\n",[10,865,866,868,869,872,873,876,877,880,881,884,885,888,889,892,893,895],{},[14,867,28],{}," performs an ",[262,870,871],{},"editable"," install of your project by default (equivalent to ",[14,874,875],{},"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,878,879],{},"bin\u002F"," (or ",[14,882,883],{},"Scripts\\"," on Windows). Because the install is editable, edits to ",[14,886,887],{},"cli.py"," take effect immediately — no reinstall needed. What ",[262,890,891],{},"does"," require a re-run of ",[14,894,28],{}," is changing the scripts table itself, since the wrapper is generated from that metadata.",[35,897,899,900,903],{"id":898},"testing-the-entry-point-with-poetry-run-and-an-activated-shell","Testing the entry point with ",[14,901,902],{},"poetry run"," and an activated shell",[10,905,906],{},"You have two ways to invoke the command inside the managed environment without publishing anything.",[10,908,909,910,912],{},"The quickest is ",[14,911,902],{},", which runs a single command in the environment:",[110,914,916],{"className":781,"code":915,"language":783,"meta":115,"style":115},"$ poetry run mycli --help\n$ poetry run mycli hello --name world\n",[14,917,918,931],{"__ignoreMap":115},[119,919,920,922,924,926,928],{"class":121,"line":122},[119,921,790],{"class":129},[119,923,793],{"class":147},[119,925,845],{"class":147},[119,927,829],{"class":147},[119,929,930],{"class":359}," --help\n",[119,932,933,935,937,939,941,943,945],{"class":121,"line":141},[119,934,790],{"class":129},[119,936,793],{"class":147},[119,938,845],{"class":147},[119,940,829],{"class":147},[119,942,353],{"class":147},[119,944,852],{"class":359},[119,946,947],{"class":147}," world\n",[10,949,950,951,954,955,958],{},"If you are iterating and want the command available across many invocations, activate the environment instead. On Poetry 2.x this is the ",[14,952,953],{},"env activate"," command (older versions used ",[14,956,957],{},"poetry shell","):",[110,960,962],{"className":781,"code":961,"language":783,"meta":115,"style":115},"$ 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,963,964,984,1001,1008,1024],{"__ignoreMap":115},[119,965,966,968,971,974,976,979,982],{"class":121,"line":122},[119,967,790],{"class":129},[119,969,970],{"class":147}," eval",[119,972,973],{"class":125}," $(",[119,975,135],{"class":129},[119,977,978],{"class":147}," env",[119,980,981],{"class":147}," activate",[119,983,329],{"class":125},[119,985,986,988,991,994,996,998],{"class":121,"line":141},[119,987,514],{"class":125},[119,989,990],{"class":129},"mycli-py3.11",[119,992,993],{"class":125},") ",[119,995,790],{"class":129},[119,997,829],{"class":147},[119,999,1000],{"class":147}," hello\n",[119,1002,1003,1005],{"class":121,"line":151},[119,1004,860],{"class":129},[119,1006,1007],{"class":147}," world!\n",[119,1009,1010,1012,1014,1016,1018,1021],{"class":121,"line":160},[119,1011,514],{"class":125},[119,1013,990],{"class":129},[119,1015,993],{"class":125},[119,1017,790],{"class":129},[119,1019,1020],{"class":147}," which",[119,1022,1023],{"class":147}," mycli\n",[119,1025,1026],{"class":121,"line":169},[119,1027,1028],{"class":129},"\u002Fhome\u002Fyou\u002F.cache\u002Fpypoetry\u002Fvirtualenvs\u002Fmycli-py3.11\u002Fbin\u002Fmycli\n",[10,1030,1031,1034,1035,1037,1038,1041,1042,1044,1045,1047,1048,1051],{},[14,1032,1033],{},"which mycli"," pointing inside the Poetry virtualenv is the proof that the wrapper was generated and put on ",[14,1036,32],{},". If the command is ",[262,1039,1040],{},"not"," found after ",[14,1043,28],{},", the usual causes are a typo in the object reference, a package that was not actually included (check the ",[14,1046,761],{}," setting), or forgetting that the current project must be installed — ",[14,1049,1050],{},"poetry install --no-root"," deliberately skips your package and therefore its scripts.",[35,1053,1055],{"id":1054},"verifying-the-object-reference-resolves","Verifying the object reference resolves",[10,1057,1058],{},"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,1060,1062],{"className":781,"code":1061,"language":783,"meta":115,"style":115},"# 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,1063,1064,1070,1087],{"__ignoreMap":115},[119,1065,1066],{"class":121,"line":122},[119,1067,1069],{"class":1068},"sJ8bj","# Does the module import and does the callable exist?\n",[119,1071,1072,1074,1076,1078,1081,1084],{"class":121,"line":141},[119,1073,790],{"class":129},[119,1075,793],{"class":147},[119,1077,845],{"class":147},[119,1079,1080],{"class":147}," python",[119,1082,1083],{"class":359}," -c",[119,1085,1086],{"class":147}," \"from mycli.cli import app; print(app)\"\n",[119,1088,1089,1092,1095],{"class":121,"line":151},[119,1090,1091],{"class":298},"\u003C",[119,1093,1094],{"class":125},"typer.main.Typer object at 0x...",[119,1096,1097],{"class":298},">\n",[10,1099,1100,1101,442,1104,1107,1108,1110,1111,53],{},"If that line raises ",[14,1102,1103],{},"ModuleNotFoundError",[14,1105,1106],{},"ImportError",", your ",[14,1109,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 ",[577,1112,1114],{"href":1113},"\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,1116,1117,1118,1121,1122,1125],{},"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,1119,1120],{},"entry_points.txt",", and reading it back confirms the exact command-to-callable mapping a user's ",[14,1123,1124],{},"pip install"," will register:",[110,1127,1129],{"className":781,"code":1128,"language":783,"meta":115,"style":115},"$ 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,1130,1131,1140,1151,1156,1161,1166,1171,1176],{"__ignoreMap":115},[119,1132,1133,1135,1137],{"class":121,"line":122},[119,1134,790],{"class":129},[119,1136,793],{"class":147},[119,1138,1139],{"class":147}," build\n",[119,1141,1142,1144,1146,1148],{"class":121,"line":141},[119,1143,790],{"class":129},[119,1145,1080],{"class":147},[119,1147,1083],{"class":359},[119,1149,1150],{"class":147}," \"\n",[119,1152,1153],{"class":121,"line":151},[119,1154,1155],{"class":147},"from importlib.metadata import distribution\n",[119,1157,1158],{"class":121,"line":160},[119,1159,1160],{"class":147},"d = distribution('mycli')\n",[119,1162,1163],{"class":121,"line":169},[119,1164,1165],{"class":147},"for ep in d.entry_points.select(group='console_scripts'):\n",[119,1167,1168],{"class":121,"line":187},[119,1169,1170],{"class":147},"    print(ep.name, '->', ep.value)\n",[119,1172,1173],{"class":121,"line":194},[119,1174,1175],{"class":147},"\"\n",[119,1177,1178,1180,1183,1186],{"class":121,"line":212},[119,1179,83],{"class":129},[119,1181,1182],{"class":125}," -",[119,1184,1185],{"class":298},">",[119,1187,1188],{"class":147}," mycli.cli:app\n",[10,1190,1191,1192,1195,1196,1199,1200,1202],{},"Seeing ",[14,1193,1194],{},"mycli -> mycli.cli:app"," printed from the ",[262,1197,1198],{},"installed"," metadata — not just the source ",[14,1201,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,1204,1206],{"id":1205},"scripts-vs-plugins-two-different-mechanisms","Scripts vs plugins: two different mechanisms",[10,1208,1209,1211,1212,1214,1215,1218,1219,1222,1223,1225,1226,1229,1230,1233,1234,1238,1239,1242],{},[14,1210,48],{}," \u002F ",[14,1213,64],{}," register ",[262,1216,1217],{},"console scripts"," — commands your user runs. They are unrelated to Poetry's own ",[262,1220,1221],{},"plugins",", which extend the ",[14,1224,135],{}," tool itself and are declared under ",[14,1227,1228],{},"[tool.poetry.plugins]"," against a named entry-point group. If you are building an extensible CLI where third parties register subcommands into ",[262,1231,1232],{},"your"," tool, that is the same underlying entry-point-group mechanism, covered in ",[577,1235,1237],{"href":1236},"\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 ",[262,1240,1241],{},"groups"," let other packages plug into a host application.",[35,1244,1246],{"id":1245},"production-notes","Production notes",[40,1248,1249,1269,1281,1290,1303,1319],{},[43,1250,1251,1254,1255,1258,1259,1261,1262,1264,1265,1268],{},[58,1252,1253],{},"The target must accept zero arguments."," ",[14,1256,1257],{},"mycli = \"mycli.cli:main\""," calls ",[14,1260,574],{}," with no args. Frameworks parse ",[14,1263,94],{}," internally, so a ",[14,1266,1267],{},"main(argv)"," signature will crash the wrapper.",[43,1270,1271,1277,1278,1280],{},[58,1272,1273,1274,1276],{},"Prefer ",[14,1275,64],{}," for new projects."," It is portable across build backends; reserve ",[14,1279,48],{}," for repos still on poetry-core \u003C 2.0.",[43,1282,1283,1289],{},[58,1284,1285,1286,1288],{},"Re-run ",[14,1287,28],{}," after editing the scripts table."," Source edits are picked up automatically (editable install), but wrapper regeneration is not.",[43,1291,1292,1295,1296,520,1299,1302],{},[58,1293,1294],{},"Multiple commands are fine."," Add more lines to the table (",[14,1297,1298],{},"mycli = ...",[14,1300,1301],{},"mycli-admin = ...",") to ship several executables from one package.",[43,1304,1305,1308,1309,1311,1312,1315,1316,1318],{},[58,1306,1307],{},"Cross-platform wrappers differ."," Poetry writes a ",[14,1310,879],{}," script on Unix and a ",[14,1313,1314],{},".exe"," shim in ",[14,1317,883],{}," on Windows; both resolve the same object reference, so no per-OS code is needed.",[43,1320,1321,1324,1325,1328,1329,1332],{},[58,1322,1323],{},"Don't hardcode the entry point in tests."," Test the callable directly (",[14,1326,1327],{},"from mycli.cli import app",") using Click's ",[14,1330,1331],{},"CliRunner"," or Typer's test runner rather than shelling out to the installed command.",[35,1334,1336],{"id":1335},"related","Related",[40,1338,1339,1346,1352,1357],{},[43,1340,1341,1345],{},[577,1342,1344],{"href":1343},"\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,1347,1348,1351],{},[577,1349,1350],{"href":579},"Best practices for Python CLI entry points"," — how to design the callable your scripts table points at.",[43,1353,1354,1356],{},[577,1355,766],{"href":765}," — the same scripts table seen from uv's side.",[43,1358,1359,1362],{},[577,1360,1361],{"href":1236},"Plugin architectures for extensible CLIs"," — entry-point groups for letting third parties extend your CLI.",[1364,1365,1366],"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":115,"searchDepth":141,"depth":141,"links":1368},[1369,1370,1371,1373,1375,1377,1378,1379,1380],{"id":37,"depth":141,"text":38},{"id":101,"depth":141,"text":102},{"id":583,"depth":141,"text":1372},"The PEP 621 equivalent: [project.scripts]",{"id":769,"depth":141,"text":1374},"poetry install makes the command available",{"id":898,"depth":141,"text":1376},"Testing the entry point with poetry run and an activated shell",{"id":1054,"depth":141,"text":1055},{"id":1205,"depth":141,"text":1206},{"id":1245,"depth":141,"text":1246},{"id":1335,"depth":141,"text":1336},"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":1382},"project-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis\u002Findex",[135,1391,1392,1393,1394],"entry-points","cli","packaging","pyproject","3xApwSWoG-yqxBZUgO3t35BtcimDGH4y_-0bGDIHnqk",[1397,1400,1403,1406,1409,1412,1415,1418,1421,1424,1427,1430,1433,1436,1439,1442,1445,1448,1451,1454,1457,1460,1463,1466,1469,1472,1475,1478,1480,1483,1486,1489,1492,1495,1498,1501,1504,1507,1510,1513,1516,1519,1522,1525,1526,1529,1532,1535,1537,1540,1543],{"path":1398,"title":1399},"\u002Fabout","About Python CLI Toolcraft",{"path":1401,"title":1402},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1404,"title":1405},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1407,"title":1408},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1410,"title":1411},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1413,"title":1414},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1416,"title":1417},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1419,"title":1420},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1422,"title":1423},"\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":1425,"title":1426},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1428,"title":1429},"\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":1431,"title":1432},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1434,"title":1435},"\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":1437,"title":1438},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1440,"title":1441},"\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":1443,"title":1444},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1446,"title":1447},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1449,"title":1450},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1452,"title":1453},"\u002F","Python CLI Toolcraft",{"path":1455,"title":1456},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1458,"title":1459},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1461,"title":1462},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1464,"title":1465},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1467,"title":1468},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1470,"title":1471},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1473,"title":1474},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1476,"title":1477},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1479,"title":1350},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points",{"path":1481,"title":1482},"\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":1484,"title":1485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1487,"title":1488},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1490,"title":1491},"\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":1493,"title":1494},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1496,"title":1497},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1499,"title":1500},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1502,"title":1503},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1505,"title":1506},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1508,"title":1509},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1511,"title":1512},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1514,"title":1515},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1517,"title":1518},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1520,"title":1521},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1523,"title":1524},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1387,"title":5},{"path":1527,"title":1528},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1530,"title":1531},"\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":1533,"title":1534},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1536,"title":766},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools",{"path":1538,"title":1539},"\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":1541,"title":1542},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1544,"title":1545},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867199]