Using workspaces
Inspired by the Cargo concept of the same name, a workspace is "a collection of one or more packages, called workspace members, that are managed together."
Workspaces organize large codebases by splitting them into multiple packages with common dependencies. Think: a FastAPI-based web application, alongside a series of libraries that are versioned and maintained as separate Python packages, all in the same Git repository.
In a workspace, each package defines its own pyproject.toml, but the workspace shares a single
lockfile, ensuring that the workspace operates with a consistent set of dependencies.
As such, fyn lock operates on the entire workspace at once, while fyn run and fyn sync operate
on the workspace root by default. Both accept a --package argument, allowing you to target a
particular workspace member from any workspace directory. For named project tasks,
fyn run --workspace <task> targets the child members together.
Getting started
To create a workspace, add a tool.fyn.workspace table to a pyproject.toml, which will implicitly
create a workspace rooted at that package.
Tip
By default, running fyn init inside an existing package will add the newly created member to the workspace, creating a tool.fyn.workspace table in the workspace root if it doesn't already exist.
In defining a workspace, you must specify the members (required) and exclude (optional) keys,
which direct the workspace to include or exclude specific directories as members respectively, and
accept lists of globs:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.fyn.sources]
bird-feeder = { workspace = true }
[tool.fyn.workspace]
members = ["packages/*"]
exclude = ["packages/seeds"]
Every directory included by the members globs (and not excluded by the exclude globs) must
contain a pyproject.toml file. However, workspace members can be either
applications or libraries; both are supported in
the workspace context.
Every workspace needs a root, which is also a workspace member. In the above example, albatross
is the workspace root, and the workspace members include all projects under the packages
directory, except seeds.
By default, fyn run and fyn sync operate on the workspace root. For example, in the above
example, fyn run and fyn run --package albatross would be equivalent, while
fyn run --package bird-feeder would run the command in the bird-feeder package.
Running tasks across workspace members
Each workspace member can define its own named tasks in [tool.fyn.tasks]:
[tool.fyn.tasks]
test = "pytest -q"
check = ["ruff check .", "pytest -q"]
Task definitions are member-local; tasks from the workspace root are not inherited by child members. Run a task in every child member that defines it with:
Unless --no-sync is supplied, fyn synchronizes the workspace environment once with all workspace
packages installed before executing the tasks. It does not perform a separate sync for each member.
Because every member uses that shared environment, console scripts with the same name are ambiguous
when published by more than one workspace package. Prefer unique script names or explicit module
commands such as python -m package in those tasks.
fyn derives execution order from the same active resolution graph used to synchronize the workspace.
It therefore honors environment markers, selected extras and dependency groups, extras activated
transitively (such as library[feature]), source overrides, and active { workspace = true }
sources. The selected target platform is used when evaluating markers. If an application declares a
library member as a workspace dependency, the library's task finishes before the application's task
starts. Members that are independent of one another run in parallel by default. Use --sequential
when tasks must run only one at a time:
Only child members that define the requested task are selected. The workspace root is deliberately excluded, even when it defines a task with the same name. This lets the root expose a convenient aggregate task without recursively invoking itself:
With that root task, fyn run test delegates to the selected child members.
Filtering workspace tasks
Use --filter with exact package names to restrict the run. For a workspace with child packages
named bird-feeder and web-api, the option can be repeated, accept a comma-separated list, or use
both forms together:
$ fyn run --workspace --filter bird-feeder test
$ fyn run --workspace --filter bird-feeder,web-api test
$ fyn run --workspace --filter bird-feeder --filter web-api test
Every filter must name a child workspace member, and every filtered member must define the requested task. Declared dependency ordering is applied among the selected members.
Add --include-dependencies to include each filtered package's active transitive workspace
dependencies, or --include-dependents to include packages that transitively depend on it:
$ fyn run --workspace --filter web-api --include-dependencies test
$ fyn run --workspace --filter shared-lib --include-dependents test
Only expanded members that define the requested task run. This means a filtered package can be a graph anchor without defining the task itself. The expansion flags are mutually exclusive.
Task definitions come from each selected member's current pyproject.toml. Ordering and expansion
come from the active resolved graph. --no-sync requires an existing fyn.lock, which lets fyn
construct that graph without updating the lockfile or environment.
Listing workspace tasks
List tasks grouped by child workspace member with:
The workspace root is excluded from this listing. --filter can be combined with --list-tasks to
inspect only selected child members:
See Running commands in projects for task-definition forms, command-sequence behavior, chained tasks, environments, and argument passing.
Workspace sources
Within a workspace, dependencies on workspace members are facilitated via
tool.fyn.sources, as in:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.fyn.sources]
bird-feeder = { workspace = true }
[tool.fyn.workspace]
members = ["packages/*"]
[build-system]
requires = ["fyn_build>=0.10.17,<0.11.0"]
build-backend = "fyn_build"
In this example, the albatross project depends on the bird-feeder project, which is a member of
the workspace. The workspace = true key-value pair in the tool.fyn.sources table indicates the
bird-feeder dependency should be provided by the workspace, rather than fetched from PyPI or
another registry.
Note
Dependencies between workspace members are editable.
Any tool.fyn.sources definitions in the workspace root apply to all members, unless overridden in
the tool.fyn.sources of a specific member. For example, given the following pyproject.toml:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.fyn.sources]
bird-feeder = { workspace = true }
tqdm = { git = "https://github.com/tqdm/tqdm" }
[tool.fyn.workspace]
members = ["packages/*"]
[build-system]
requires = ["fyn_build>=0.10.17,<0.11.0"]
build-backend = "fyn_build"
Every workspace member would, by default, install tqdm from GitHub, unless a specific member
overrides the tqdm entry in its own tool.fyn.sources table.
Note
If a workspace member provides tool.fyn.sources for some dependency, it will ignore any
tool.fyn.sources for the same dependency in the workspace root, even if the member's source is
limited by a marker that doesn't match the current
platform.
Workspace layouts
The most common workspace layout can be thought of as a root project with a series of accompanying libraries.
For example, continuing with the above example, this workspace has an explicit root at albatross,
with two libraries (bird-feeder and seeds) in the packages directory:
albatross
├── packages
│ ├── bird-feeder
│ │ ├── pyproject.toml
│ │ └── src
│ │ └── bird_feeder
│ │ ├── __init__.py
│ │ └── foo.py
│ └── seeds
│ ├── pyproject.toml
│ └── src
│ └── seeds
│ ├── __init__.py
│ └── bar.py
├── pyproject.toml
├── README.md
├── fyn.lock
└── src
└── albatross
└── main.py
Since seeds was excluded in the pyproject.toml, the workspace has two members total: albatross
(the root) and bird-feeder.
When (not) to use workspaces
Workspaces are intended to facilitate the development of multiple interconnected packages within a single repository. As a codebase grows in complexity, it can be helpful to split it into smaller, composable packages, each with their own dependencies and version constraints.
Workspaces help enforce isolation and separation of concerns. For example, in fyn, we have separate packages for the core library and the command-line interface, enabling us to test the core library independently of the CLI, and vice versa.
Other common use cases for workspaces include:
- A library with a performance-critical subroutine implemented in an extension module (Rust, C++, etc.).
- A library with a plugin system, where each plugin is a separate workspace package with a dependency on the root.
Workspaces are not suited for cases in which members have conflicting requirements, or desire a
separate virtual environment for each member. In this case, path dependencies are often preferable.
For example, rather than grouping albatross and its members in a workspace, you can always define
each package as its own independent project, with inter-package dependencies defined as path
dependencies in tool.fyn.sources:
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]
[tool.fyn.sources]
bird-feeder = { path = "packages/bird-feeder" }
[build-system]
requires = ["fyn_build>=0.10.17,<0.11.0"]
build-backend = "fyn_build"
This approach conveys many of the same benefits, but allows for more fine-grained control over
dependency resolution and virtual environment management (with the downside that fyn run --package
is no longer available; instead, commands must be run from the relevant package directory).
Finally, fyn's workspaces enforce a single requires-python for the entire workspace, taking the
intersection of all members' requires-python values. If you need to support testing a given member
on a Python version that isn't supported by the rest of the workspace, you may need to use fyn pip
to install that member in a separate virtual environment.
Note
As Python does not provide dependency isolation, fyn can't ensure that a package uses its declared dependencies and nothing else. For workspaces specifically, fyn can't ensure that packages don't import dependencies declared by another workspace member.