uv scripts: micro-production situationship

Grigorii Osipov

Sometimes you are not trying to start a Python project at all.

Sometimes you are just working in a Terraform repository, doing Terraform things, and then a small problem appears that Terraform alone does not solve elegantly. Not a big enough problem to justify a full Python package, but annoying enough that a quick script suddenly starts looking very reasonable.

That is where uv scripts started making a lot of sense to me.

Under the hood, this workflow is enabled by PEP 723, which standardises inline script metadata. In practice though, the real experience is uv. It is the tool that makes the whole thing pleasant enough to actually use.


When a small script does not justify a full Python setup

Recently I worked in a Terraform repository where JSON configuration was generated through Terraform templates. The small version of that problem looks like this.

variable "project_name" {  default = "ducklake"}locals {  config = templatefile("${path.module}/config.json", {    project = var.project_name  })}
variable "project_name" {  default = "ducklake"}locals {  config = templatefile("${path.module}/config.json", {    project = var.project_name  })}
variable "project_name" {  default = "ducklake"}locals {  config = templatefile("${path.module}/config.json", {    project = var.project_name  })}
{  "service": "${project}-api",  "environment": "production"}
{  "service": "${project}-api",  "environment": "production"}
{  "service": "${project}-api",  "environment": "production"}

Terraform fills in the variables and produces the final JSON used by the infrastructure code.

For a tiny JSON file, this is barely worth thinking about. For a multi-thousand-line Grafana dashboard adapted for deployment across multiple accounts, it becomes much more annoying.

At that point I wanted a small local helper script that could inspect the rendered output before shipping it. Then the script grew slightly more useful. It validated the JSON structure, extracted panel names, and compared the generated output against a known-good baseline.

This was useful enough to keep, but not important enough to turn the repository into a Python project.

That is the small niche where uv scripts fit extremely well.

The overhead of a modern Python project

A typical modern Python project might look something like this:

├── .gitignore├── CHANGELOG.md├── README.md├── VERSION.md├── pyproject.toml├── src│   └── terraform_json_validator│       ├── __init__.py│       └── main.py└── uv.lock
├── .gitignore├── CHANGELOG.md├── README.md├── VERSION.md├── pyproject.toml├── src│   └── terraform_json_validator│       ├── __init__.py│       └── main.py└── uv.lock
├── .gitignore├── CHANGELOG.md├── README.md├── VERSION.md├── pyproject.toml├── src│   └── terraform_json_validator│       ├── __init__.py│       └── main.py└── uv.lock

This is already quite minimal by modern standards. I am not even listing tests, CI, documentation setup, editor config, or anything more ambitious.

Still, for a tiny utility script inside a Terraform repository, this is a lot.

To do it the conventional way, I would have to introduce multiple root-level files, project structure, packaging metadata, and all the usual Python machinery just to support one script. That is perfectly reasonable when the repository is actually a Python repository. It feels much less reasonable when the repository is primarily infrastructure code and the Python part exists only to solve one specific problem.

That is the part I find interesting. There is a huge gap between a throwaway one-off script and a full Python project. In reality, a lot of useful internal tooling lives somewhere in the middle.

uv scripts as the middle ground

I remembered a Reddit post about one-off scripts and went looking through the uv documentation. The suggested starting point was this:

uv init --script json_parser.py --python 3.12

That generates a file like this:

# /// script# requires-python = ">=3.12"# dependencies = []# ///def main() -> None:    print("Hello from json_parser.py!")if __name__ == "__main__":    main()
# /// script# requires-python = ">=3.12"# dependencies = []# ///def main() -> None:    print("Hello from json_parser.py!")if __name__ == "__main__":    main()
# /// script# requires-python = ">=3.12"# dependencies = []# ///def main() -> None:    print("Hello from json_parser.py!")if __name__ == "__main__":    main()

Instead of creating a separate pyproject.toml, the script carries its own metadata inline. The Python version, dependencies, and later even a lock file can all be tied directly to that one script.

Adding dependencies is just as straightforward:

uv add — script json_parser.py “requests<3” “rich”

Which updates the metadata block directly inside the script:

# /// script# dependencies = [#   "requests<3",#   "rich",# ]# ///...
# /// script# dependencies = [#   "requests<3",#   "rich",# ]# ///...
# /// script# dependencies = [#   "requests<3",#   "rich",# ]# ///...

That is a very neat balance. The script stays a script, but it is no longer pretending dependencies do not exist.

If package versions are particularly sensitive, they can also be locked:

uv lock --script json_parser.py

This produces a json_parser.py.lock file, which plays the same role as a normal uv.lock file in a regular project.

That matters more than it might initially seem. A one-file script is nice. A one-file script with reproducible dependencies is something I would be far happier to commit to a shared repository.

And to not forget, the explicit way to run the script is:

uv run --script json_parser.py
uv run --script json_parser.py
uv run --script json_parser.py

That is probably the clearest version for a shared repository. It makes the uv dependency obvious and does not hide too much behind Unix script behaviour. And if you still want to go all the way and make it feel properly script-like, you can add a shebang:

#!/usr/bin/env -S uv run --script## /// script# requires-python = ">=3.12"# dependencies = []# ///...
#!/usr/bin/env -S uv run --script## /// script# requires-python = ">=3.12"# dependencies = []# ///...
#!/usr/bin/env -S uv run --script## /// script# requires-python = ">=3.12"# dependencies = []# ///...

Then make it executable:

chmod +x json_parser.py

At that point, anyone with uv installed can run the file directly in fully script-like behaviour.

./json_parser.py
./json_parser.py
./json_parser.py

This is probably the one part where I am undecided. It is convenient, but it also hides some abstraction and assumes familiarity with uv. Whether that trade-off is acceptable depends on the team and the repository. I think it is fine as long as the repository makes that expectation clear.

Small does not have to mean disposable

Even if the script remains a single file, I still do not think that means it should be treated casually. Production code usually benefits from tests, linting, formatting, and type checking. For a small utility script, I would probably skip tests unless the logic grows large enough to justify them. The overhead can outweigh the value quite quickly.

However, I would not feel comfortable committing Python code to a shared repository without at least formatting, linting, and type checking. It is largely because these tools communicate intent. They make it clearer how the code should behave, how it should be structured, and what guarantees the author expects. In practice, that makes the script easier to read, easier to maintain, and much easier for both humans and automated tools to modify safely.

If a hook configuration already exists, I would just add to it. Otherwise, a small prek.toml file is enough.

At this point, someone might reasonably ask whether this is overengineering for a simple script. I would say no, almost every time.

Spending an extra ten minutes to add formatting, linting, and type checking dramatically improves readability and maintainability. If the file is going to live in a shared repository and other people are expected to touch it, then it deserves at least that level of care.

Why I like this pattern

What I like about uv scripts is not just that they make small scripts easy. Plenty of things make small scripts easy.

What I like is that they make small scripts respectable.

They give you a middle ground between two extremes:

  • a throwaway script with undocumented dependencies and vague expectations

  • a fully scaffolded Python project that feels disproportionate to the problem

With uv scripts, a single file can still declare its Python version, manage its dependencies, lock them when needed, participate in formatting and linting, and sit comfortably inside a repository that is not otherwise Python-first.


Conclusion

In the end, this gives me something that feels production-adjacent without pretending to be a full application.

It lives as a single file, yet still benefits from dependency management, reproducibility, formatting, linting, and type checking. It can be executed directly, version-controlled alongside infrastructure code, and shared with coworkers without introducing a full Python project structure into a repository that does not need one.

I think that is the real appeal of uv scripts and yet another checkmark for reasons to use uv.

PEP 723 is what makes the inline metadata standard possible. uv is what makes it actually pleasant. And together they create a very practical sweet spot for what I can only describe as micro-production code.

Latest

uv scripts: micro-production situationship

Portable S3 security for EU clouds

Portable S3 security for EU clouds using JWT, OPA, and temporary credentials without hyperscaler lock-in.

Data Platforms for humans

Data platforms fail when people are ignored. Why kindness and communication matter as much as architecture.

Subscribe to our monthly newsletter

Subscribe to our monthly newsletter

Subscribe to our monthly newsletter

Belgium

Vismarkt 17, 3000 Leuven - HQ
Boomgaardstraat 115, 2018 Antwerpen


Vat. BE.0667.976.246

© 2025 Dataminded. All rights reserved.


Belgium

Vismarkt 17, 3000 Leuven - HQ
Boomgaardstraat 115, 2018 Antwerpen

Vat. BE.0667.976.246

© 2025 Dataminded. All rights reserved.


Belgium

Vismarkt 17, 3000 Leuven - HQ
Boomgaardstraat 115, 2018 Antwerpen

Vat. BE.0667.976.246

© 2025 Dataminded. All rights reserved.