Quick rundown for the unfamiliar:
Give it a command as a list of strings (e.g., subprocess.run(["echo", "foo"]).)
It takes a bunch of flags, but the most useful (but not immediately obvious) ones are:
check=True: Raise an error if the command fails
capture_output=True: Captures stdout/stderr on the CompletedProcess
text=True: Automatically convert the stdout/stderr bytes to strings
By default, subprocess.run will print the stdout/stderr to the script's output (like bash, basically), so I only bother with capture_output if I need information in the output for a later step.Basically you can just `from sh import [command]` and then have an installed binary command available as function
from sh import ifconfig
print(ifconfig("eth0"))https://docs.astral.sh/uv/guides/scripts/#using-different-py...
But if a script I write needs to use arrays, sets, hashtable or processes many files - I use Nim[0]. It's a compiled systems-programming language that feels like a scripting language:
- Nim is easy to write and reads almost like a pseudocode.
- Nim is very portable language, runs almost anywhere C can run (both compiler and programs).
- `nim r script.nim` to compile and run (cached on subsequent runs) or use a shebang `#!/bin/env -S nim r`
- Nim programs are fast to compile (use debug mode and tcc compiler for almost instant compile times)
- Nim scripts run very fast <10ms (something that was very annoying to me with bash and Python)
- good chances you don't need external dependencies, because stdlib is batteries included and full of goodies.
- if you need external deps - just statically link them and distribute a cross-compiled binary (use zigcc[1] for easy Nim cross-compilation).
[0] - https://nim-lang.org
> Python 3 is installed on basically every machine out there.
> Python will work the same on all the machines you run your script on
No, no, and no.
C# scripts let you reference packages in a comment at the top of the file, for example:
https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-...
https://avilpage.com/2025/04/learn-python-uv-in-100-seconds....
# /// script
# dependencies = [
# "cowsay",
# ]
# ///
import cowsay
cowsay.cow("Hello World")
Then: uv run cowscript.py
It manages a disposable hidden virtual environment automatically, via a very fast symlink-based caching mechanism.You can also add a shebang line so you can execute it directly:
#!/usr/bin/env -S uv run --script
#
# /// script
# dependencies = ["cowsay"]
# ///
import cowsay
cowsay.cow("Hello World")
Then: chmod 755 cowscript
./cowscriptIf you are installing packages, then starting with installing uv should be fine.
Might as well rewrite all your scripts in Rust too while you're layering on unholy amounts of complexity.
I’m so thankful to see a flake.nix file in every single cool project on code forges.
Seeing that flake.nix badge of complexity lets me know a project will be a nightmare to set up and will break every other week. It's usually right next to the Cargo.toml badge with 400 dependencies underneath.
Then you can do e.g. use other persons python scripts without modifying their shebang.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
import requests
from rich.pretty import pprint
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])Generally the only nontrivial scripting I ever do is associated with a larger project, so I often already have a pyproject.toml and a UV environment, and I just add the dependencies to the dev group.
It just feels strange that C# of all languages is now a better scripting tool than Python, at least out of the box. I did notice uv has exactly the feature I'm looking for, though it's obviously third-party:
https://docs.astral.sh/uv/guides/scripts/#declaring-script-d...
Is everyone just using uv now instead of pip, perhaps? Or is just another alongside pipenv, conda, poetry, etc.? (Python's not my main these days, so I'm out of the loop.)
That said, a lot of very complicated things are actually written in bash. Distrobox I think is for example.
They're only complicated BECAUSE they're written in bash. If they were written in Python they would be much less complicated, more modular, able to use many existing industrial strength well tested python modules instead of rolling their own ad-hoc ones, and much easier to maintain.
I suspect conda still has some market share too but I've never needed it.
once the script is non-trivial, 'install' it using pipx, in editable mode when you work on the script and as normal pipx installed cli utility otherwise.
the venv part is then completely under the hood.
Not only does bash not have a module system like python, or a vast ecosystem of modules like python, but also that it's much too weak and brittle a language to implement most of those modules that Python has, and can't even call native code libraries directly.
Even with just its standard built in "batteries included" libraries and no extension modules or native code modules, Python is still much more powerful than bash and easier to code and maintain.
If you're complaining about having to install Python modules, you're usually already doing something that's impossible or incredibly difficult to do in bash anyway.
Even something as simple and essential as fetching files via http or parsing json. Bash has to call out to other programs to do that, but you have to install those programs too, and while Python can certainly call out to curl or wget or jq, it doesn't have to, since it has all that and more built in.
There really is no comparison, because bash loses along so many dimensions at once compared to Python.
The same way you handle them with bash?
Install them?
What are we talking about here?
Use that.
cp version.template build/version.txt
sed -i "s/@VERSION@/${COMMIT_TAG:-dev}/" build/version.txt
sed -i "s/@BUILD_DATE@/${BUILD_DATE}/" build/version.txt
Eww! Mutating a file in place, and needing a GNU extension for it. sed \
-e "s/@VERSION@/${COMMIT_TAG:-dev}/" \
-e "s/@BUILD_DATE@/${BUILD_DATE}/" \
< build/version.template > build/version.txtPython:
forty_two = ( 42 )
tuple_containing_forty_two = ( 42, ) 42,
This expression is a tuple all by itself.Would be cool if python had a pipe operator though.
The back ticks in ruby is pretty ergonomic too. Wish python had a simpler way to run commands. Kind of tedious to look up subprocess run arguments and also break things up into arrays.
For example,
subprocess.run(“rm -rf ~/ some file”, shell=True)
and subprocess.run([“rm”, “-rf”, “~/ some file”])
have significant different behavior.