The author’s point about “not caring about pip vs poetry vs uv” is missing that uv directly supports this use case, including PyPI dependencies, and all you need is uv and your preferred Python version installed: https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to...
I thought that too, but I think the tricky bit is if you're a non-python user, this isn't yet obvious.
If you've never used Clojure and start a Clojure project, you will almost definitely find advice telling you to use Leiningen.
For Python, if you search online you might find someone saying to use uv, but also potentially venv, poetry or hatch. I definitely think uv is taking over, but its not yet ubiquitous.
Ironically, I actually had a similar thing installing Go the other day. I'd never used Go before, and installed it using apt only to find that version was too old and I'd done it wrong.
Although in that case, it was a much quicker resolution than I think anyone fighting with virtual environments would have.
That's my experience. I'm not a Python developer, and installing Python programs has been a mess for decades, so I'd rather stay away from the language than try another new tool.
Over the years, I've used setup.py, pip, pipenv (which kept crashing though it was an official recommendation), manual venv+pip (or virtualenv? I vaguely remember there were 2 similar tools and none was part of a minimal Python install). Does uv work in all of these cases? The uv doc pointed out by the GP is vague about legacy projects, though I've just skimmed through the long page.
IIRC, Python tools didn't share their data across projects, so they could build the same heavy dependencies multiple times. I've also seen projects with incomplete dependencies (installed through Conda, IIRC) which were a major pain to get working. For many years, the only simple and sane way to run some Python code was in a Docker image, which has its own drawbacks.
> IIRC, Python tools didn't share their data across projects, so they could build the same heavy dependencies multiple times.
One of the neatest features of uv is that it uses clever symlinking tricks so if you have a dozen different Python environments all with the same dependency there's only one copy of that dependency on disk.
I've moved over mostly to uv too, using `uv pip` when needed but mostly sticking with `uv add`. But as soon as you start using `uv pip` you end up with all the drawbacks of `uv pip`, namely that whatever you pass after can affect earlier dependency resolutions too. Running `uv pip install dep-a` and then `... dep-b` isn't the same as `... dep-b` first and then `... dep-a`, or the same as `uv pip install dep-a dep-b` which coming from an environment that does proper dependency resolution and have workspaces, can be really confusing.
This is more of a pip issue than uv though, and `uv pip` is still preferable in my mind, but seems Python package management will forever be a mess, not even the bandaid uv can fix things like these.
However... scripting requires (in my experience), a different ergonomic to shippable software. I can't quite put my finger on it, but bash feels very scriptable, go feels very shippable, python is somewhere in the middle, ruby is closer to bash, rust is up near go on the shippable end.
Good scripting is a mixture of OS-level constructs available to me in the syntax I'm in (bash obviously is just using OS commands with syntactic sugar to create conditional, loops and variables), and the kinds of problems where I don't feel I need a whole lot of tooling: LSPs, test coverage, whatever. It's languages that encourage quick, dirty, throwaway code that allows me to get that one-off job done the guy in sales needs on a Thursday so we can close the month out.
Go doesn't feel like that. If I'm building something in Go I want to bring tests along for the ride, I want to build a proper build pipeline somewhere, I want a release process.
I don't think I've thought about language ergonomics in this sense quite like this before, I'm curious what others think.
Talking about Python "somewhere in the middle" - I had a demo of a simple webview gtk app I wanted to run on vanilla Debian setup last night.. so I did the canonical-thing-of-the-month and used uv to instantiate a venv and pull the dependencies. Then attempted to run the code.. mayhem. Errors indicating that the right things were in place but that the code still couldn't run (?) and finally Python Core Dumped.. OK. This is (in some shape or form) what happens every single time I give Python a fresh go for an idea. Eventually Golang is more verbose (and I don't particularly like the mod.go system either) but once things compile.. they run. They don't attempt running or require xyz OS specific hack.
Gtk makes that simple python program way more complex since it'll need more than pure-python dependencies.
It's really a huge pain point in python. Pure python dependencies are amazingly easy to use, but there's a lot of packages that depend on either c extensions that need to be built or have OS dependencies. It's gotten better with wheels and manylinux builds, but you can still shoot your foot off pretty easily.
I've had similar issues with anaconda, once upon a time. I've hit a critical roadblock that ruined my day with every single Python dependency/environment tool except basic venv + requirements.txt, I think. That gets in the way the least but it's also not very helpful, you're stuck with requirements.txt which tends to be error-prone to manage.
Maybe the ergonomics of writing code is less of a problem if you have a quick way of asking an LLM to do the edits? We can optimize for readability instead.
More specifically, for the readability of code written by an LLM.
Have to disagree, "technically" yes, both are interpreted languages, but the ergonomics and mental overhead of doing certain things are wildly different:
In python, doing math or complex string or collection operations is usually a simple oneliner, but calling shell commands or other OS processes requires fiddling with the subprocess module, writing ad-hoc streaming loops, etc - don't even start with piping several commands together.
Bash is the opposite: As long as your task can be structured as a series of shell commands, it absolutely shines - but as soon as you require custom data manipulation in any form, you'll run into awkward edge cases and arbitrary restrictions - even for things that are absolutely basic in other languages.
I don’t really understand the initial impetus. I like scripting in Python. That’s one of the things it’s good at. You can extremely quickly write up a simple script to perform some task, not worrying about types, memory, yada yada yada. I don’t like using Python as the main language for a large application.
It seems to be Linux specific (does it even work on other unix like OSes?) and Linux usually has a system Python which is reasonably stable for things you need scripting for, whereas this requires go to be installed.
You could also use shell scripting or Python or another scripting language. While Python is not great at backward compatibility most scripts will have very few issues. Shell scripts are backward compatible as are many other scripting languages are very backward compatible (e.g. TCL) and they areG more likely to be preinstalled. If you are installing Go you could just install uv and use Python.
The article does say "I started this post out mostly trolling" which is part of it, but mostly the motivation would be that you have a strong preference for Go.
Expected a rant, got a life-pro-tip. Enough for a good happy new year.
That said, we can abuse the same trick for any languages that treats `//` as comment.
List of some practical(?) languages: C/C++, Java, JavaScript, Rust, Swift, Kotlin, ObjC, D, F#, GLSL/HLSL, Groovy
Personally, among those languages, GLSL sounds most interesting. A single-GLSL graphics demo is always inspiring. (Something like https://www.shadertoy.com/ )
Also, let’s not forget that we can do something similar using block comment(`/* … */`). An example in C:
For Swift there’s even a project[1] that allows running scripts that have external dependencies (posting the fork because the upstream is mostly dead).
I think it’s uv’s equivalent, but for Swift.
(Also Swift specifically supports an actual shebang for Swift scripts.)
So the entire reason why this is not a "real" shebang and instead takes the roundtrip through the shell is because the Go runtime would trip over the # character?
I think this points to some shortcomings of the shebang mechanism itself: That it expects the shebang line to be present and adhering a specific structure - but then passes the entire file with the line to the interpreter where the interpreter has to process (and hopefully ignore) the line again.
I know that situations where one piece of text is parsed by multiple different systems are intellectually interesting and give lots of opportunities for cleverness - but I think the straightforward solution would be to avoid such situations.
So maybe the linux devs should consider adding a new form for the shebang where the first line is just stripped before passing the file contents to the interpreter.
I love it. I'm using Go to handle building full stack javascript apps, which actually works great since esbuild can be used directly inside a Go program. The issue is that it's a dependency, so I settled for having a go mod file and running it directly with Go. If somehow these dependencies could be resolved without an explicit module configured (say, it was inline in the go file itself) it would be perfect. Alas, it will probably never happen.
That being said...use Go for scripting. It's fantastic. If you don't need any third party libraries this approach seems really clean.
I make computers do things, but I never act like my stuff is the only stuff that makes things happen. There is a huge software stack of which my work is just the final pieces.
The problem with calling it “full stack” (even if it has a widely understood meaning) is that it implicitly puts the people doing the actual lower-level work on a pedestal. It creates the impression that if this is already “full stack,” then things like device drivers, operating systems, or foundational libraries must be some kind of arcane magic reserved only for experts, which they aren’t.
The term “full stack” works fine within its usual context, but when viewed more broadly, it becomes misleading and, in my opinion, problematic.
Or, alternatively, it ignores and devalues the existence of these parts. In both cases, it's a weird "othering" of software below a certain line in the, ahem, full stack.
And it's okay. It doesn't mean it should be this way for everyone else.
It is pretty common (and been so for at least two decades) for web devs to differentiate like so: backend, frontend or both. This "both" part almost always is replaced by "full stack".
When people say this they just mean they do both parts of a web app and have no ill will or neglect towards systems programmers or engineers working on a power plant.
You don't even need to end the file in `.go` or the like when using shebangs, and any self-respecting editor will be good at parsing out shebangs to identify file types (... well, Emacs seems to do it well enough for me)
no need to name your program foo.go when you could just name it foo
Tcc even supports that with `#!/usr/local/bin/tcc -run`, although I don't understand people who use c or go for "scripting", when python, ruby, TCL or perl have much superior ergonomics.
This was a relatively old project that used a C program as build system / meta generator. All you needed was a working C compiler (and your shell to execute the first line). From there, it built and ran a program that generated various tables and some source code, followed by compiling the actual program. The final program used a runtime reflection system, which was set up by the generated tables and code from the first stage.
The main reason was to do all this without any dependencies beyond a C compiler and some POSIX standard library.
Tangent but... I kinda like the Python language. What I don't like about Python is the way environments are managed.
This is something I generally believe, but I think it's particularly important for things like languages and runtimes: the idea of installing things "on" the OS or the system needs to die.
Per-workspace or per-package environment the way Go, Rust, etc. does it is correct. Installing packages globally is wrong.
There should not be such a thing as "globally." Ideally the global OS should be immutable or nearly so, with the only exception being maybe hardware driver stuff.
(Yes I know there's stuff like conda, but that's yet another thing to fix a fundamentally broken paradigm.)
Using `uv` with python is significantly safer and better. At least you get null safety. Sure, you can't run at the speed of light, but at least you can have some decent non-halfarsed-retrofitted type checking in your script.
The author’s point about “not caring about pip vs poetry vs uv” is missing that uv directly supports this use case, including PyPI dependencies, and all you need is uv and your preferred Python version installed: https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to...
Actually you can go one better:
Then you don't even need python installed. uv will install the version of python you specified and run the command.alternatively, uv lets you do this:
I thought that too, but I think the tricky bit is if you're a non-python user, this isn't yet obvious.
If you've never used Clojure and start a Clojure project, you will almost definitely find advice telling you to use Leiningen.
For Python, if you search online you might find someone saying to use uv, but also potentially venv, poetry or hatch. I definitely think uv is taking over, but its not yet ubiquitous.
Ironically, I actually had a similar thing installing Go the other day. I'd never used Go before, and installed it using apt only to find that version was too old and I'd done it wrong.
Although in that case, it was a much quicker resolution than I think anyone fighting with virtual environments would have.
That's my experience. I'm not a Python developer, and installing Python programs has been a mess for decades, so I'd rather stay away from the language than try another new tool.
Over the years, I've used setup.py, pip, pipenv (which kept crashing though it was an official recommendation), manual venv+pip (or virtualenv? I vaguely remember there were 2 similar tools and none was part of a minimal Python install). Does uv work in all of these cases? The uv doc pointed out by the GP is vague about legacy projects, though I've just skimmed through the long page.
IIRC, Python tools didn't share their data across projects, so they could build the same heavy dependencies multiple times. I've also seen projects with incomplete dependencies (installed through Conda, IIRC) which were a major pain to get working. For many years, the only simple and sane way to run some Python code was in a Docker image, which has its own drawbacks.
> Does uv work in all of these cases?
Yes. The goal of uv is to defuck the python ecosystem and they're doing a very good job at it so far.
> IIRC, Python tools didn't share their data across projects, so they could build the same heavy dependencies multiple times.
One of the neatest features of uv is that it uses clever symlinking tricks so if you have a dozen different Python environments all with the same dependency there's only one copy of that dependency on disk.
Do you think a non-python user would piece it together if the shebang line reveals what tool to use?
I've moved over mostly to uv too, using `uv pip` when needed but mostly sticking with `uv add`. But as soon as you start using `uv pip` you end up with all the drawbacks of `uv pip`, namely that whatever you pass after can affect earlier dependency resolutions too. Running `uv pip install dep-a` and then `... dep-b` isn't the same as `... dep-b` first and then `... dep-a`, or the same as `uv pip install dep-a dep-b` which coming from an environment that does proper dependency resolution and have workspaces, can be really confusing.
This is more of a pip issue than uv though, and `uv pip` is still preferable in my mind, but seems Python package management will forever be a mess, not even the bandaid uv can fix things like these.
Won't those dependencies then be global? With potential conflicts as a result?
uv uses a global cache but hardlinks the dependencies for your script into a temp venv that is only for your script, so its still pretty fast.
Nope! uv takes care of that. uv is a work of art.
Then I should seriously take a look at it. I figured it was just another package manager.
Mad genius stuff, this.
However... scripting requires (in my experience), a different ergonomic to shippable software. I can't quite put my finger on it, but bash feels very scriptable, go feels very shippable, python is somewhere in the middle, ruby is closer to bash, rust is up near go on the shippable end.
Good scripting is a mixture of OS-level constructs available to me in the syntax I'm in (bash obviously is just using OS commands with syntactic sugar to create conditional, loops and variables), and the kinds of problems where I don't feel I need a whole lot of tooling: LSPs, test coverage, whatever. It's languages that encourage quick, dirty, throwaway code that allows me to get that one-off job done the guy in sales needs on a Thursday so we can close the month out.
Go doesn't feel like that. If I'm building something in Go I want to bring tests along for the ride, I want to build a proper build pipeline somewhere, I want a release process.
I don't think I've thought about language ergonomics in this sense quite like this before, I'm curious what others think.
Talking about Python "somewhere in the middle" - I had a demo of a simple webview gtk app I wanted to run on vanilla Debian setup last night.. so I did the canonical-thing-of-the-month and used uv to instantiate a venv and pull the dependencies. Then attempted to run the code.. mayhem. Errors indicating that the right things were in place but that the code still couldn't run (?) and finally Python Core Dumped.. OK. This is (in some shape or form) what happens every single time I give Python a fresh go for an idea. Eventually Golang is more verbose (and I don't particularly like the mod.go system either) but once things compile.. they run. They don't attempt running or require xyz OS specific hack.
Gtk makes that simple python program way more complex since it'll need more than pure-python dependencies.
It's really a huge pain point in python. Pure python dependencies are amazingly easy to use, but there's a lot of packages that depend on either c extensions that need to be built or have OS dependencies. It's gotten better with wheels and manylinux builds, but you can still shoot your foot off pretty easily.
How were the dependencies specified? What kind of files were provided for you to instantiate the venv?
I haven't had the same issue with anaconda. Give it a try.
I've had similar issues with anaconda, once upon a time. I've hit a critical roadblock that ruined my day with every single Python dependency/environment tool except basic venv + requirements.txt, I think. That gets in the way the least but it's also not very helpful, you're stuck with requirements.txt which tends to be error-prone to manage.
Maybe the ergonomics of writing code is less of a problem if you have a quick way of asking an LLM to do the edits? We can optimize for readability instead.
More specifically, for the readability of code written by an LLM.
> bash obviously is just using OS commands with syntactic sugar
No, bash is technically not "more" OS than e.g. Python. It just happens that bash is (often) the default shell in the terminal emulator.
Have to disagree, "technically" yes, both are interpreted languages, but the ergonomics and mental overhead of doing certain things are wildly different:
In python, doing math or complex string or collection operations is usually a simple oneliner, but calling shell commands or other OS processes requires fiddling with the subprocess module, writing ad-hoc streaming loops, etc - don't even start with piping several commands together.
Bash is the opposite: As long as your task can be structured as a series of shell commands, it absolutely shines - but as soon as you require custom data manipulation in any form, you'll run into awkward edge cases and arbitrary restrictions - even for things that are absolutely basic in other languages.
I don’t really understand the initial impetus. I like scripting in Python. That’s one of the things it’s good at. You can extremely quickly write up a simple script to perform some task, not worrying about types, memory, yada yada yada. I don’t like using Python as the main language for a large application.
It seems to be Linux specific (does it even work on other unix like OSes?) and Linux usually has a system Python which is reasonably stable for things you need scripting for, whereas this requires go to be installed.
You could also use shell scripting or Python or another scripting language. While Python is not great at backward compatibility most scripts will have very few issues. Shell scripts are backward compatible as are many other scripting languages are very backward compatible (e.g. TCL) and they areG more likely to be preinstalled. If you are installing Go you could just install uv and use Python.
The article does say "I started this post out mostly trolling" which is part of it, but mostly the motivation would be that you have a strong preference for Go.
I love scripting in Python too. I just hate trying to install other people’s scripts.
If they use https://packaging.python.org/en/latest/specifications/inline... then it becomes a breeze to run with uv. Not even a thing.
You do have to worry about types, you always do. You have to know, what did this function return, what can you do with it.
When you know well the language, you dont need to search for this info for basic types, because you remember them.
But that's also true for typed languages.
Expected a rant, got a life-pro-tip. Enough for a good happy new year.
That said, we can abuse the same trick for any languages that treats `//` as comment.
List of some practical(?) languages: C/C++, Java, JavaScript, Rust, Swift, Kotlin, ObjC, D, F#, GLSL/HLSL, Groovy
Personally, among those languages, GLSL sounds most interesting. A single-GLSL graphics demo is always inspiring. (Something like https://www.shadertoy.com/ )
Also, let’s not forget that we can do something similar using block comment(`/* … */`). An example in C:
/*/../usr/bin/env gcc "$0" "$@"; ./a.out; rm -vf a.out; exit; */
#include <stdio.h>
int main() { printf("Hello World!\n"); return 0; }
For Swift there’s even a project[1] that allows running scripts that have external dependencies (posting the fork because the upstream is mostly dead).
I think it’s uv’s equivalent, but for Swift.
(Also Swift specifically supports an actual shebang for Swift scripts.)
[1] https://github.com/xcode-actions/swift-sh
So the entire reason why this is not a "real" shebang and instead takes the roundtrip through the shell is because the Go runtime would trip over the # character?
I think this points to some shortcomings of the shebang mechanism itself: That it expects the shebang line to be present and adhering a specific structure - but then passes the entire file with the line to the interpreter where the interpreter has to process (and hopefully ignore) the line again.
I know that situations where one piece of text is parsed by multiple different systems are intellectually interesting and give lots of opportunities for cleverness - but I think the straightforward solution would be to avoid such situations.
So maybe the linux devs should consider adding a new form for the shebang where the first line is just stripped before passing the file contents to the interpreter.
I love it. I'm using Go to handle building full stack javascript apps, which actually works great since esbuild can be used directly inside a Go program. The issue is that it's a dependency, so I settled for having a go mod file and running it directly with Go. If somehow these dependencies could be resolved without an explicit module configured (say, it was inline in the go file itself) it would be perfect. Alas, it will probably never happen.
That being said...use Go for scripting. It's fantastic. If you don't need any third party libraries this approach seems really clean.
>full stack
Device drivers, task switching, filesystem, memory management and all?
dwight shrute detected
Yes. Yes, I'm doing all of that with Javascript :P
Don't be that guy.
I am going to be that guy.
I make computers do things, but I never act like my stuff is the only stuff that makes things happen. There is a huge software stack of which my work is just the final pieces.
The term "full stack" has a widely well understood meaning, you're being pedantic
The problem with calling it “full stack” (even if it has a widely understood meaning) is that it implicitly puts the people doing the actual lower-level work on a pedestal. It creates the impression that if this is already “full stack,” then things like device drivers, operating systems, or foundational libraries must be some kind of arcane magic reserved only for experts, which they aren’t.
The term “full stack” works fine within its usual context, but when viewed more broadly, it becomes misleading and, in my opinion, problematic.
Or, alternatively, it ignores and devalues the existence of these parts. In both cases, it's a weird "othering" of software below a certain line in the, ahem, full stack.
It doesn't for me and I don't think that my subculture of computing uses similarly myopic terms.
>It doesn't for me
And it's okay. It doesn't mean it should be this way for everyone else.
It is pretty common (and been so for at least two decades) for web devs to differentiate like so: backend, frontend or both. This "both" part almost always is replaced by "full stack".
When people say this they just mean they do both parts of a web app and have no ill will or neglect towards systems programmers or engineers working on a power plant.
Where did your subculture come from, Pedanticville?
Mostly not web-based software, written in compiled languages
I agree with you in sentiment - the term "full-stack" is odd and a little too grandiose for its meaning.
But it is already established in the industry, and fighting it is unlikely to yield any positive outcomes.
what's even cooler is when the language comes with first class support for this: https://www.erlang.org/docs/18/man/escript
Or the venerable https://babashka.org/
You don't even need to end the file in `.go` or the like when using shebangs, and any self-respecting editor will be good at parsing out shebangs to identify file types (... well, Emacs seems to do it well enough for me)
no need to name your program foo.go when you could just name it foo
https://github.com/erning/gorun lets you embed `go.mod` and `go.sum` and have dependencies in Go scripts.
You can do the same[1] with .Net Core for those of us who like that.
[1]: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals...
dotnet was always really good for this. There were a bunch of third party tools that have done this since the 90s like toolsack.
I think Java can run uncompiled text scripts now too
Back in the days, I've seen that with C files, which are compiled on the fly to a temporary file an run.
Something like //usr/bin/gcc -o main "$0"; ./main "$@"; exit
Tcc even supports that with `#!/usr/local/bin/tcc -run`, although I don't understand people who use c or go for "scripting", when python, ruby, TCL or perl have much superior ergonomics.
This was a relatively old project that used a C program as build system / meta generator. All you needed was a working C compiler (and your shell to execute the first line). From there, it built and ran a program that generated various tables and some source code, followed by compiling the actual program. The final program used a runtime reflection system, which was set up by the generated tables and code from the first stage.
The main reason was to do all this without any dependencies beyond a C compiler and some POSIX standard library.
Official stance about supporting interpreter mode for the reference https://github.com/golang/go/issues/24118
May I...
augroup fix autocmd! autocmd BufWritePost *.go \ if getline(1) =~# '^// usr/bin/' \ | call setline(1, substitute(getline(1), '^// ', '//', '')) \ | silent! write \ | endif augroup END
The following would probably be more portable:
Note, the exit code isn't passed through due to: https://github.com/golang/go/issues/13440To quote the blog in question:
> How true this is, is a topic I dare not enter.
One suggestion: change `exit` to `exit $?` so an exit code is passed back to the shell.
I've been meaning to port some dotfiles utils over to go, I think I'll give this a shot.
Tangent but... I kinda like the Python language. What I don't like about Python is the way environments are managed.
This is something I generally believe, but I think it's particularly important for things like languages and runtimes: the idea of installing things "on" the OS or the system needs to die.
Per-workspace or per-package environment the way Go, Rust, etc. does it is correct. Installing packages globally is wrong.
There should not be such a thing as "globally." Ideally the global OS should be immutable or nearly so, with the only exception being maybe hardware driver stuff.
(Yes I know there's stuff like conda, but that's yet another thing to fix a fundamentally broken paradigm.)
Using `uv` with python is significantly safer and better. At least you get null safety. Sure, you can't run at the speed of light, but at least you can have some decent non-halfarsed-retrofitted type checking in your script.
I think you're mistaking Go for some other language.
> I started this post out mostly trolling, but the more I've thought about it's not a terrible idea.
I feel like this is the unofficial Go motto, and it almost always ends up being a terrible idea.
Now try to call some C++ code from your Go script…