Skip to content

uv init: Project layouts, defaults and terminology #8178

@MikeHart85

Description

@MikeHart85

Thank you for working on this wonderful tool.

I'd like to suggest several changes to uv init arguments and defaults, with the following goals:

  • Use consistent and established terminology
  • Options to support common usage patterns, so seasoned Python developers can have things their way
  • Defaults which guide Python newcomers towards best practices, hint at available features

Much of this revolves around separating the concept of project directory structure ("layout" in Python) from project purpose and contents ("library", "application", etc), as well as disambiguating the overloaded term "package". Currently these terms are conflated and inconsistent in uv.

Apologies in advance for the massive wall of text.

Current behavior

"Application" (default):

$ uv init [--app] project-name
$ tree project-name/
project-name/
├── hello.py
├── pyproject.toml  # only [project] (builds don't work due to misnamed hello.py)
└── README.md

"Library":

$ uv init --lib project-name
project-name/
├── pyproject.toml  # [project], [build-system]
├── README.md
└── src
    └── project_name
        ├── __init__.py  # contains "hello" code
        └── py.typed

"Application Package":

$ uv init [--app] --package project-name  # --app appears to do nothing here, in spite of docs suggesting it
project-name/
├── pyproject.toml  # [project], [project.scripts], [build-system]
├── README.md
└── src
    └── project_name
        └── __init__.py  # contains "hello" code

Suggested behavior

Src layout (default):

$ uv init [--layout=src] project-name
project-name/
├── pyproject.toml  # [project], [project.scripts], [build-system]
├── README.md
└── src
    └── project_name
        └── __init__.py  # empty
        └── example.py   # def say_hello(): print("...")

Flat layout:

$ uv init --layout=flat project-name
project-name/
├── pyproject.toml  # [project], [project.scripts], [build-system]
├── README.md
└── project_name
    └── __init__.py  # empty
    └── example.py   # def say_hello(): print("...")

Single module layout:

$ uv init --layout=single project-name
project-name/
├── pyproject.toml  # [project], [project.scripts], [build-system]
├── README.md
└── project_name.py  # def say_hello(): print("...") 
                     # NOTE: file name MUST be project_name.py (it replaces the package)

Bare / no layout:

$ uv init --layout=<bare|none> project-name  # maybe pick one rather than allowing either one
project-name/
├── pyproject.toml  # [project]
└── README.md
  • Add --no-entrypoint to suppress [project.scripts]
    • Consider adding --entrypoint=name to customize key name under [project.scripts], defaulting to project-name
  • Add --build-system=<hatchling|...|none>, with hatchling being default, and none to suppress [build-system]
    • Consider adding --no-build-system, synonymous with --build-system=none, for symmetry
    • Remove --no-package
  • Add --typed to create a py.typed
  • Remove --package
  • Remove --app, or:
    • Make synonymous with --entrypoint=project-name
    • Consider generating example code to be more "app-like"
    • Consider __main__.py [1] instead of example.py, but that may unnecessarily confuse newcomers
  • Remove --lib, or:
    • Make synonymous with --no-entrypoint
    • Consider generating example code to be more "lib-like"
    • Consider implying --typed (IMO explicit uv init --lib --typed would be better)

I would just remove --lib and --app to keep things simple.

Reasoning for suggestions

  • "Entry point" is the established term for shortcuts to functions that ultimately become commands when installed [2]
  • "Layout" is the established term for project directory structure [3], [4]
    • Src layout should be default
      • It is the most robust, avoids import conflicts, most suitable for serious projects
      • Prevents needing to restructure when project outgrows other layouts
      • Uv eliminates its main downside (very easy to get up-to-date REPL):
      • uv run python
      • >>> import project_name
    • Flat layout is also quite popular, and currently not available in uv init
    • Single module (current --app, default) layout is the most limited
      • It should most certainly not be the default
      • Has issues and limitations which will confuse newcomers
      • Should really only be used for single file projects, if at all
      • Currently broken for builds due to example file always being hello.py
      • Changing file to project_name.py allows builds to work
    • --layout=none for custom layouts, notebook projects, etc
  • Project layout is not related to whether it is an "app" or "lib"
    • Any layout can be an application or a library
    • Project contents and usage determine whether it is an application, or library, or both
    • Not as clear cut in Python as "executable" vs "library" projects in compiled languages, shouldn't attempt to force that pattern since it doesn't fit
    • "App" and "Application" terms should probably be reserved for possible integration with tools like PyInstaller, cxfreeze, etc (IE, generating a self-contained binary executable for distribution)
    • A library isn't required to have PEP 561 compliant py.typed
  • Current use of --package conflates it with build-system/distribution, src layout, and absence of py.typed
    • A "package" in Python is simply a directory with an __init__.py [5]
    • Ideally, the term "package" should not be used for anything else, to avoid confusion
    • A package doesn't have to be distributable / have a build-system
    • Single module (IE, non-package) distributions are a thing (hence --layout=single with build-system)
      • A prominent example would be six, which is a single module library: PyPI GitHub
  • __init__.py should not contain general purpose code
    • __init__.py is a special purpose file, and putting arbitrary code there can easily have unintended side effects
    • Its meant for initializing packages, making symbols available at package level, __all__, etc [6]
    • Putting the hello world example there can mislead newcomers into thinking this is where all their code belongs
  • Don't see a reason not to include entry point and build system by default in all layouts which support it
    • Can easily be removed / ignored / switched off if not needed
    • Default presence shows newcomers what is possible / where it belongs
    • Reduces friction when a project graduates from hobby to distribution
  • Layouts are mutually exclusive, hence grouping them under one argument with a value
    • Less room for confusion than having multiple differently named options, which may or may not be mixable
  • All three layouts (src, flat, single) work with uv run project-name and uv build as shown
    • Only uv init needs to change, to support generating them

Context

Discussion in the latter floated using "distribution" (ala PDM) instead of "package" (ala Poetry) in some contexts, but it seems "package" won out. I would argue "distribution" would have been the correct choice. As mentioned, "package" has a very specific meaning in Python, not strictly related to whether the project is built for distribution.

Thanks for considering.

Metadata

Metadata

Assignees

Labels

needs-decisionUndecided if this should be done

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions