API for discovering environments for a project

First off, thanks to all the thoughtful posts!

I’m not ready to get into config order, if because I know Steve has argued to leave that up to the front-end tools instead of standardizing.

So JSON-RPC with some defined function endpoints where you are specifying how to launch the server. It’s an interesting idea!

I’m not sure if this needs to be part of any spec. If you only want to run one command then run one command and exit. If you’re talking about surfacing the functionality as individual CLI commands, a CLI tool could provide that and act as a unified CLI API across all tools. That gets you both your scripting and AI support.

That seems reasonable as tools can perform whatever search they want for a pyproject.toml file.

It also makes it a little nicer to the conda community.

I don’t know if always accepting an array makes sense, e.g. …

… does what when given an array of environments? Runs the same command concurrently?

I will also say that taking a JSON-RPC approach for execution will require a broader API surface to handle stdin, killing processes, etc. Steve’s PEP 517 approach avoids this somewhat by defining a protocol for interacting with the running command (very likely like subprocess and/or asyncio subprocesses).

That would be my assumption assuming people don’t opt out of having any workflow tool run automatically.

Is “consumers” front-ends? In that case I would say it’s fine to create a virtual environment if no tool is specified.

I can tell I have thought about this. :grin: Regardless of whether a server protocol or PEP 517 approach is taken, I expect front-ends to be able to run the thing they need if it isn’t installed. How they do that can be up to the tool (e.g. create an ephemeral virtual environment, use pipx, etc.). I would expect there to be a way to specify what should be available to run the tool.

I agree with Steve’s sentiment that …

So I have always expected a way to specify what to make available from PyPI if that’s where your tool exists.

This has been a design goal for the PEP from the start. My personal workflow is very much like Paul’s workflow and I will make sure I can configure my own setup via my own code so I can have this (and anyone else can have their own).

Not if I can help it. I would assume anything dealing with processes takes a list of arguments so quoting is avoided.

I do assume some CLI tool will be created to provide a CLI API for working with whatever approach may be chosen (if any approach is chosen at all by me; I can still choose to stick with PEP 832 as-is an let some other fool person take this more complicated approach on). As such, that CLI tool can provide such a baseline, simple experience when no workflow tool is specified.

This is also my assumption. This sort of environment management is necessary for inline script metadata, so I don’t think there’s anything to concern ourselves with. It’s basically an implementation detail for a front-end to handle in whatever way they choose.

Both approaches would allow you to do this, they just differ in what it would take to make it happen. And as I said, I will make sure this is possible because I personally want this as well. Otherwise it’s either having the front-end tool having a way to choose a per-machine or person config or standardizing how to select such a config.

I’ll address this below, but I don’t think having a Python shim around uv is a big ask since that shim could use subprocess to call uv and be its own package.

Either way, I would expect a CLI tool that can directly utilize either API for whatever reason someone/thing has to want to call a workflow tool via the API.

I do as well as I will work to make it happen. :wink:

I would assume you could you use inline script metadata, so I don’t think multiple copies; you just configure one path to use.


I want to make a few things abundantly clear that are independent of either approach. One is people should have a way to point at something bespoke on their machine that implements the API the way they want to do it. And you should be able to override per-project and you should be able to set a default fallback. Whether this is in the spec or something people expect/demand front-ends is open for debate.

Two, I expect there to be a way to declare what would need to be installed. That way the front-end can do what it needs to do to get the tool to run it.

Three, I expect there to be a CLI tool that can work with the API. That helps AI models, people who want to script things without being tool-specific, etc. I also expect there to be a supported fallback.

Now, what are the key differences between the two approaches? Let’s look at it from the angle of Hatch (written in Python) and uv (written in Rust).

With Ofek’s approach, uv implements a JSON-RPC server. With Hatch it also has to implement such a server.

With Steve’s approach, uv implements a wrapper. With Hatch it can more directly expose its own code.

This then also applies to what it takes for someone to do their own solution. With Ofek’s approach people are doing a server however they want, while with Steve’s they are implementing a Python class.

In terms of bootstrapping, Ofek’s expects to (potentially) more self-contained when a workflow tool ships a self-contained binary (but not everyone does). Steve assumes getting Python on a machine is either already done or will be easy enough in the future thanks to relocatable builds eventually coming from python.org (but finding Python on a machine can be hard and I have not made relocatable builds happen yet).

I think those are the differentiators. Honestly the key question for me is whether any workflow tools would do either approach and do they have a preference or would refuse to implement one of the approaches. I don’t want to pester the other workflow tool maintainers until we agree that what I’m saying is accurate so I can give them a reasonable “what do you think” post to read and give feedback on.

2 Likes

Yeah, that’s a better idea.

It would run the same command in each environment sequentially. Consumers can optimize for parallelism.

That sounds reasonable.

I agree with the assessment that follows but I think you missed a rather large difference in user experience.

To be absolutely clear:

If we do not go with an approach that allows for interactive bi-directional communication then workflow tools written in Python will always be less responsive and downright sluggish compared to others. Users of such tools will never have a snappy experience when clicking the button next to a test function, running one of their predefined tasks with a hotkey, etc.

To put it in biased terms, if we do this then there is no way for Hatch to match the user expectations of tools like uv.


If we use my language here:

Then I’d be fine with requiring a list of Python package requirements as long as each spec supports the full syntax i.e. allows for a local path to a directory, a Git VCS URI, etc.

It’s not that much of a lift to turn a script into a package with a minimal pyproject.toml nowadays although I’m curious what Paul thinks about this idea. I think it’s a fair compromise.

Agreed with the rest of your post broadly. I don’t think I need to make my imagined approach any clearer here, so happy to leave it with decision makers and clarify when requested.

Let’s not confuse “tool” with “workflow tool”, or as I’ve been stealing from PEP 517 “frontend” vs “backend” (or the concrete examples, “VS Code” vs “Hatch”).

The “CLI tool” is a frontend, and so if it provides the default behaviour, then it’s the reference implementation and we expect every other frontend to go off and implement it as well.

I suggested publishing a backend, so if no more specific backend is provided then that can be installed and it will do the job. Front-ends don’t need to implement any default behaviour other than “substitute this name and do my usual thing” (or whatever a user has requested they do).

I can’t disagree with the assessment that it’s a fair compromise. However, it’s almost certainly enough of a barrier that for me, personally, it would stop me from using the feature. I’ll explain why, simply for information - I’m not asking for special consideration here. But given that I was surprised when Brett said his workflow was like mine, maybe my reservations here will also resonate with others, which might affect the choices we make.

My problem is that when I write a single file script, I can do what I like with it - copy it wherever I need, put it on PATH or run it by name from a directory, point to it as config in a tool, etc. Single file scripts need no infrastructure and no workflow. Most critically, I don’t need to keep the “source” anywhere - the script is the source.

When I make a script into a package, I suddenly have a source and a built artifact (probably a wheel, maybe something else). I have to build the package and use that, but I also need to retain and manage the source. I need to “publish” the built project, even if that publishing is just putting it in a directory somewhere on my PC. I need to rebuild and redeploy after I change the source. I need to choose a build backend. Heck, I have to choose a name for my project (many of my personal scripts are called things as meaningless as pyr.py or even x.py - don’t judge me :slightly_smiling_face:) There’s basically a whole workflow needed beyond just writing the script. For small personal projects, I’m generally very bad at these things - the sort of management I do for published projects, or bigger pieces of code, doesn’t feel relevant for a single personal script.

2 Likes

I assumed it was the other way round. The “CLI tool” was something users would configure with a backend (the same as you would with VS Code, for example), and then you could use it to provide backend-agnostic access to environments - env-tool list, env-tool create xxx, env-tool run some command.

Which is to say, please can we be a lot more explicit about terminology, because concepts are getting mixed up and it’s getting very hard to follow what people are talking about…

1 Like

I made the same assumptions as Paul and therefore was quite confused reading the past few messages. So everyone is on the same page, there are two parts:

  1. A tool that exposes a low-level environment management API. Examples: Hatch, uv, Conda, Nix, etc.
  2. A caller of the tool. Examples: VS Code, Jupyter, a CLI wrapping the API, etc.

I’ve been referring to the caller as a “consumer” of the tool’s API while inconsistently referring to the tool itself as an “environment manager” or “workflow tool”. If we were to reuse build system terminology then the tool would be the backend and the API consumer would be the frontend.

I think those terms don’t fit quite nicely however and I truly dislike the Python API approach, so I’d like to use different terms. Let’s agree on the following split terminology:

  • CLI API: the tool is a “workflow server” and the caller is a “workflow client”
  • Python API: the tool is a “workflow backend” and the caller is a “workflow frontend”

Those advocating for the latter may choose different terminology of course! That’s just the preference I perceived based on the desire to emulate PEP 517.

Got it! I think technically we’re still fine then because of this:

Assuming that’s still Steve’s opinion, then we don’t need to make requires a required field. However, folks seem to want (and I now agree) for there to be a default workflow server package requirement that:

  • Uses a virtual environment located in a .venv directory at the project root
  • Uses the standard library’s venv module for environment creation

So I think there would be 3 possible scenarios.

  1. requires is not defined: the field defaults to an array containing the default server as the only dependency

  2. requires is defined: here’s my earlier comment (note that the logic would be the same for the not defined scenario so we would have to clean up the language of that field):

  3. A new field/override config that would disable the usage of the requires field so that clients don’t modify PATH when invoking the server CLI.

What do we think about that? It’s possible that if we go with this then Steve’s comment about the requires field being only for intent might be unnecessary. We could say that clients must respect the dependencies, not just a best-effort thing, unless the user disabled it.

1 Like

This where Ofek and I are fundamentally aligned. I do not want any proposal that puts hatch at a performance disadvantage before we even write a single line of code to implement it. That is not to say that Rust is the magic bullet here, Ofek and I are very good about writing high performant Python code for hatch but without bi-directional communication then there is going to be a cost to pay that will be much harder to keep up with tools written in other languages.

I agree here and I think this is the right user experience to lean into for a default. Admittedly I am less concerned with Python power users here and more concerned with the casual Python user or new to Python user. That is to say, I have full faith that Paul will find a way to use the workflow that they have already regardless of where we go because of the expertise that they have. But we should ensure that whatever the final form is that we do not make it impossible for Paul to have their standard workflow still work for them. And I do not think anyone in this thread so far has suggested a solution that would be pushing towards making that impossible.

1 Like

It should still be required in the project metadata - otherwise, there’s no context for resolving the name of the API that you’re about to invoke. I’m just saying we don’t have to mandate “workflow frontends must install exactly these packages from PyPI into an isolated virtual environment”. That approach should work,[1] but we shouldn’t try to ban other ways to achieve the same result.

I don’t want a “requirement”, I just want the package :wink: It can be implemented outside of the API definition and just referenced as “here’s our intended default behaviour, frontend tools probably want to use this as the fallback backend to keep your users happy or else they’ll get angry at you”. That way the actual behaviour, particularly as it relates to edge cases and obscure platforms, can be handled outside of the PEP process.

Why are you both assuming that the Python API can only be used once and then the runtime has to be completely shut down? I deliberately made my example a class because you can instantiate it once and do as many operations as you want - if instantiating it starts a local server and does interactive bi-directional communication, great! That’s totally allowed. If a frontend isn’t written in Python and wants to do their own bi-directional communication with a persistent Python runtime that is accessing the API, great!

If Hatch wants to document a “bypass” interface that is more efficient for frontends that want to directly interact with Hatch’s server, then you can do that - just specify the values of metadata that frontends can interpret as “use hatch --json-rpc ... directly”, and go around helping frontends to use it. Any who don’t [care about their users] will stick with the default, multi-hop API, which works but is less efficient.

Literally everything I’m pushing for is to allow free choices like this. I hope I don’t have to go around imagining how to implement this stuff for everyone, because I can, it’s just less efficient than project owners getting that this API is a minimum and not the maximum. If you want to go above and beyond, all you need to do is help the API consumer use it, and can rely on this API to signal when it should be used. The rest is up to you.

(Also FWIW, I’m not overly attached to the “frontend” and “backend” terminology, but I’m using it as consistently as I can because I think the metaphor to PEP 517 is more helpful right now than trying to invent new words.)


  1. Certainly for any public project your users should complain if it doesn’t work. Private projects using private backends and private frontends don’t have to be forced into using a public index to claim “compliance with the API”. ↩︎

How about VS Code and Hatch for terms? :wink: And I’m only half joking; I’m going to stick to those concrete terms in this discussion going forward as it’s unambiguous and people for both tools are represented here. I also don’t think anyone will be insulted if we use those two projects as running examples.

Steve beat me to the response:

I never assumed the Python API approach would be one-shot runs all the time. I expect VS Code would write its own server layer to handle all of this. Yes, it makes it like the CLI API proposal, but it is still different as its a choice instead of a requirement.

I honestly was just assuming the CLI tool could play both roles. The key point is I don’t see either approach prohibiting accessing the API from a CLI tool.

I don’t expect either approach to prevent using a single .py file because I don’t want to necessarily write anything more complicated. I’ll talk about it more below.

I think of it more like “requires if needed” at least for the CLI API case. I’ll talk about it more below.


OK, let’s talk through how each approach may work to get the differences laid out. That way we can focus on that aspect and get more targeted feedback from other workflow tool maintainers.

Getting Hatch

Python API

There would be a requirement definition in pyproject.toml. VS Code would create or reuse a virtual environment where Hatch gets installed. This does require finding a Python interpreter on the machine, although long term that shouldn’t be a concern due to relocatable builds and VS Code can already find interpreters (or even short term not a problem if you’re okay with python-build-standalone).

CLI API

VS Code can check if the first thing listed in the command is installed. If Hatch is already installed – via e.g. Homebrew, dnf, apt, etc. – then you’re done.

There would be a requirement definition in pyproject.toml. You could create a virtual environment and find the command in bin/. Otherwise you could use pipx to handle launching.

Specifying a local alternative to Hatch

Python API

You should be able to give a path to a .py file or maybe a directory as a package.

CLI API

You should be able to give a path to a .py file or maybe a directory with a __main__.py file. You could also point to any file and run it, e.g. a shell script.

VS Code interacting w/ Hatch

Python API

VS Code would launch a Python server that loaded Hatch and worked with it without having to launch Hatch for every operation.

CLI API

VS Code launches Hatch and interacts with it in its server mode, leaving the server running.

Using the API

Python API

Protocols would be defined. The simple case would be methods that return a mapping. Complicated would be a exec API which returns a subprocess-like object that handles communication, killing the process, return code, etc.

CLI API

JSON-RPC server which defines the expected functions. Simple case is getting JSON back. Complicated case it an exec API where you would need to define functions for each part you need for working with a process (e.g. communication, killing the process, return code, etc.) via some process ID.

2 Likes

Yup, some distros should do this in a “non-isolated” build. Which of course is isolated – so much that to Python tooling it looks like a raw OS-installed Python.
IMO, tools that discover environments should handle the case of the environment not being virtual.
And creating environments should be configurable enough that you can write a tool that provides an offline container instead of a venv. That’s largely equivalent to Paul’s “configure tools to use my workflow in all my projects”, but I think it’s a good case to keep in mind when deciding how configurable all this should be.

Detection should be easy. “Installing" a specific Python version means using a specific container to run the tool. [1]

Any sufficiently corporate-ish environment will have a filtered list of allowed artifacts and will need to deal with the same issue.

Which is why linting should be separate from the functional tests, and the test suite should be separate from the environment matrix. IMO that’s a reasonable step away from “works on my (CI) machine” – after all, whenever you want to add a new environment to the mix, it’s rather handy to be able to run the relevant tests in a given pre-existing environment.


  1. Even to “install” a Python package that wasn’t pre-declared, you re-run the tool in a container that has the given package installed. (Yeah, also “nuts”, but of a different kind.) ↩︎

1 Like

OK, I’m putting a time limit of deciding by Friday, June 5th as to which API approach to take back to PEP 832: virtual environment discovery to see if people even want an API approach. As such, I’m now looping in the other tool maintainers to see if they have a preference (to be clear, I’m not asking if you prefer PEP 832 as-is or an API approach, just which API approach to take back to the PEP discussion). I am also going to summarize things here for how I view the strengths of either approach so the tool maintainers don’t need to read through 70 posts :sweat_smile: (you can also feed https://discuss.python.org/raw/107186/ into some AI if you want a summary that way). Finally, there’s a poll at the end for anyone who wants to express an opinion that way which I will run for a week.

/cc @frostming (PDM) @radoering (Poetry) @zanie (uv) @carltongibson (virtualenvwrapper) @bernatgabor (virtualenv/tox) @henryiii (nox) @jezdez (conda) @lucascolley (pixi)


So this is all about what approach to take for defining an API that workflow tools can implement to expose details about what environments they manage(to start; I’m sure it could be expanded in the future). Don’t worry about the exact shape of the API for this discussion; this is about how to expose the functionality.

We have two approaches we are considering: a CLI API approach and a Python API approach. The CLI API will have workflow tools implement a JSON-RPC server and pyproject.toml will record how to launch the tool as a server (think LSP but for Python workflows). The Python API where workflow tools implement a class with methods and pyproject.toml records the entry point (think PEP 517).

Let’s start with what’s the same. Both would allow for the same underlying functionality (e.g. both can handle listing the available environments or running a Python command in a specific environment). Both allow for being run as a server to support a responsive experience (who writes that server is different, though, and will be discussed later). Both approaches will let you specify what workflow tool you want to use, either in pyproject.toml or in some way to provide a default or even override (but this is just because the tool calling a workflow tool can make that choice). Both approaches allow someone to write their own workflow tool and specify using that (albeit with different requirements on what they need to do; discussed later). Both approaches could have a CLI made for it for scripting purposes to make calls to the workflow tools.

So how do the approaches differ? The key differences are covered in CLI API for discovering environments for a project - #70 by brettcannon . I think the two key differences is how people get the workflow tool on to their machine and where does responsibility/flexibility lie?

For the CLI API approach, it has a perk that the workflow tool could simply be on the machine already. That’s a potential irritant for users if the calling tool using the workflow tool doesn’t provide a way to get the workflow tool on their behalf. How big of a deal that is depends on how hard you think it is to find a Python install on a machine or how much faith you have in me getting relocatable builds on python.org to make getting Python as easy as a download.

The other key difference is who is in charge of what. The CLI API means every workflow tool needs to implement a JSON-RPC server. That might be easy by relying on another project on e.g. PyPI, but it is another layer of stuff to do. On the other hand, it might be a preference as it’s a clear boundary of control between the calling tool and the workflow tool. Compare that to the Python API where some might view it as easier to implement, but not getting to control e.g. the event loop if the API gets imported into some running code might not be desired (if that’s even a concern; I don’t think build back-ends have shown this to be an issue, but you never know).

I think the differences come down to how much control/work the workflow tools want. The CLI API approach gives the workflow tools more control at the cost of more work (i.e. more installation control, how they implement their API behind the JSON-RPC server). Now workflow maintainers might want that control for technical reasons or because they think they need to make the lives of the calling tools as easy as possible to maximize uptake. But workflow maintainers might also not want more work if they can help it or think calling tools want more control instead. There’s also whether people think it’s easy enough to implement their own bespoke workflow tool on their machine with JSON-RPC involved and behind a command/script (with inline script metadata probably helping a lot), or the Python API is the most they are willing to do.

:person_shrugging: Both approaches have merits and I don’t see a clear winner when trying to choose between the two (and you might not like either approach and prefer PEP 832 as-is, but that’s a separate discussion), hence this post. :sweat_smile:

Which API approach do you prefer?
  • CLI API
  • Python API
0 voters
5 Likes

A reminder that the poll closes on Monday.

1 Like

I’m still unclear about whether the Python API version will require the API to be made available in a package (like PEP 517) or whether a simple Python module is sufficient.

If a package is needed, that’s a difference that would matter to me.

1 Like

My thinking was that an option for a relative path (to add to sys.path before importing the API) or even an implied import path of the root directory would cover that, with requires then only containing external dependencies (if any). Build backends were a bit different, and wanting to force arbitrary code to come from a “trusted”/vettable source made more sense, but since there’s no silent upgrade here[1].

I certainly would not want “we failed to find your module on PyPI” to be an error condition. “We failed to find it on your machine and you didn’t give us a way to find it on PyPI” is okay.

But it looks like the preference is for a CLI tool, which means you won’t be able to distribute custom instructions in your source repo anyway.


  1. Unlike someone who was previously pip installing a package getting the new behaviour one day, nobody is currently triggering the frontend tool that uses this API. ↩︎

If you’re referring to a means of declaring Python dependencies then I think that would definitely be supported. I mentioned how I see that working here:

No, I’m referring to putting an entire custom tool into your repository. Like this example on GH, which could be adapted to have an importable interface that tools would just pick up and use, rather than having to install a separate tool that can be run as a CLI to provide the same specific-to-this-project functionality.

Thanks. My question was less about needing to publish to PyPI than about whether it would need to be a wheel (which the frontend would then install “somewhere”). If a single importable .py file containing the relevant entry points is acceptable (and the PEP requires frontends to support it, so it’s not just a tool implementation choice) then I’m more OK with the Python API.

Yeah, and given that your answer simply shifted me from “slightly in favour of a CLI tool” to neutral, the fact that I asked too late to get an answer before the vote closed doesn’t actually matter in the end.

Thanks for everyone who voted! It came out 7 to 2 for a CLI API (6 to 2 if you combine the 2 Hatch maintainers both voting). I’ll start a conversation over on the PEP topic to see if people even want this.

I don’t plan on that being true (if we follow through with an API in the PEP; still need to have that discussion). Whether it’s via having a “script”/“path” key that can point to a script with inline script metadata or point to a directory with a __main__.py, pyproject.toml, or pylock.toml, there will be some way to have a repo contain custom code.

1 Like