When would you declare a command under [project.gui-scripts] instead of [project.scripts]?
answer
- Same mechanism, different reserved group
- The difference only shows on one platform
- No black window on double-click
- The windowed interpreter has no console
- So print() has nowhere to go
basics
~20 sFor a desktop application launched from a shortcut rather than a terminal. On Windows the generated launcher is bound to the windowed interpreter, so no console window appears; on Linux and macOS the two tables produce the same wrapper.
solid answer
~40 sBoth tables are the same entry-point mechanism with different reserved group names — `console_scripts` and `gui_scripts` — and both generate a launcher at install time. The difference is Windows-only: a `gui_scripts` command gets a launcher bound to `pythonw.exe`, the windowed-subsystem interpreter, so launching it from a shortcut or file association does not open a black console window. On Unix the generated wrappers are identical, so the choice is inert there. The consequence people miss is that a windowed process has no console attached: `sys.stdout` and `sys.stderr` may be `None`, so `print()` is useless and an unhandled exception during startup produces an application that simply never appears. Configure file logging and a top-level exception handler before anything else.
code
python · 6 linesfrom importlib.metadata import entry_points
gui = entry_points(group="gui_scripts")
console = entry_points(group="console_scripts")
print(f"gui_scripts: {sorted(ep.name for ep in gui)}")
print(f"console_scripts: {len(console)} installed")go deeper
Know that both tables declare commands the same way, name = "module:function", and that the GUI one exists for desktop applications rather than terminal tools.
Explain the actual difference: the reserved group name in the metadata and the windowed launcher on Windows, with no observable difference on Unix.
Bring the operational consequence — no attached console means no stdout, no stderr and no visible traceback, so file logging and a top-level exception hook are set up before anything else can fail.
Weigh whether shipping a desktop entry point is worth the support surface at all: crash reporting, log retrieval from user machines, and the per-platform packaging work that a console tool never needs.
### Two tables, one mechanism `[project.scripts]` and `[project.gui-scripts]` are the same feature with different metadata group names. A backend writes the first into the wheel's `entry_points.txt` under `console_scripts` and the second under `gui_scripts`; both entries are `command = "import.path:callable"`, and in both cases the installer generates the launcher at install time into the environment's script directory. ```toml [project.scripts] hooksrv = "hooksrv.cli:main" [project.gui-scripts] hooksrv-monitor = "hooksrv.monitor.app:main" ``` ### What differs, and where The difference is **Windows-only**. On Windows, a command declared under `console_scripts` gets a launcher bound to `python.exe`, which is a console subsystem executable — double-clicking it, or launching it from a desktop shortcut, opens a console window that stays for the life of the process. A command declared under `gui_scripts` gets a launcher bound to `pythonw.exe`, the windowed subsystem variant: no console window appears, and nothing flashes on screen while a GUI application starts. On Linux and macOS the two produce the same thing — a script whose shebang names the environment's interpreter. There is no console-window concept to suppress, so the distinction is inert. That is the honest answer to "what does it do on my Mac": nothing observable. You still declare it, because your users may not all be on your platform, and the metadata is what a Windows installer needs. ### The consequence people get wrong Under the windowed launcher **there is no console attached to the process**, so the standard streams are not connected to anything a user can see. `sys.stdout` and `sys.stderr` may be `None`, and code that assumes they are writable can fail in ways that are invisible precisely because the error message has nowhere to go. Practical rules for a `gui_scripts` target: * Do not rely on `print()` for anything that matters. Configure file logging early, before importing the parts of the app that might log during import. * Install an exception hook or a top-level `try`/`except` that surfaces failures through the GUI toolkit and the log file, otherwise a crash during startup is a program that simply never appears. * Do not read from standard input. There is nothing there. * Anything you want a developer to see during debugging should be reachable another way — the same entry point can be exposed under `[project.scripts]` with a second command name if you want a console-attached variant for support. ### Discovering them Because it is an ordinary entry-point group, the installed GUI commands are enumerable at runtime like any other: ```python from importlib.metadata import entry_points for ep in entry_points(group="gui_scripts"): print(ep.name, ep.value) ``` ### When to choose which Use `[project.scripts]` for anything that is fundamentally a command-line tool, including one that happens to open a window occasionally — you want its output, its exit status and its tracebacks in the terminal that launched it. Use `[project.gui-scripts]` for an application whose normal launch path is a shortcut, a Start-menu item or a file association, where a stray black window is a visible defect. If you ship both a daemon-style tool and a desktop monitor from the same distribution, declaring one in each table is exactly right, and costs nothing on the platforms where the distinction does not apply. ### Why it is worth knowing but rarely asked Nobody is turned down for not knowing `gui_scripts` exists; most Python work never ships a desktop GUI. It earns its place as a differentiator: a candidate who names the `pythonw` launcher and then immediately follows with "and that means you cannot print" has clearly shipped a desktop application to Windows users rather than read the table of contents of the packaging guide.
- What breaks in a gui-scripts target that logs progress with print()?Under the windowed launcher there is no console attached, so the standard streams are not connected to anything visible and may be `None`. At best the output vanishes; at worst code that assumes a writable stream raises, and because the traceback also has nowhere to go the user sees an application that never opens. Configure file logging early and install a top-level exception handler that surfaces failures through the GUI toolkit.
- Can the same function be exposed as both a console command and a GUI command?Yes. The two tables are independent maps of command name to target, so you can point a `[project.scripts]` entry and a `[project.gui-scripts]` entry at the same callable under different command names. That is a practical way to ship a support-friendly console-attached variant of a desktop app, since the console version keeps stdout, stderr and exit status where a terminal can see them.
saying these in an interview costs you the question
- Thinks gui-scripts installs or requires a GUI toolkit
- Expects a visible difference on Linux or macOS
- Assumes print() output is visible in a windowed launcher
- Believes the launcher goes somewhere other than the script directory
- Treats it as a separate mechanism rather than another entry-point group