How do you handle packages? I want scripts to a be a single file with a shebang, not a repo with a requirements.txt that I need to run in a venv. To me, this is the biggest blocker to using Python for any non-trivial scripting (which is precisely the kind where I wouldn't want to use bash), but I'd like to know how others deal with it.
C# scripts let you reference packages in a comment at the top of the file, for example:
As someone who writes a lot of python, I love uv, but isn't on nearly every system like python is, which is one of the arguments for using python here in the first place
The catch is that there could be a version mismatch between the version of Python installed on the end user's computer and the version on which the script was developed. This problem can be solved with uv, and there aren't really Python-native ways available.
Isn't the fact that bash doesn't have a wonderful ecosystem of reusable modules a much more enormous insurmountable problem than the fact that you have to install python modules? You're really missing the forest for the trees.
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.
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.
Honestly, I just use a requirements.txt file. It's not as convenient as having a single file, it's true, but that is far outweighed by how much easier Python is to use than bash. So I'm giving up a 3/10 convenience but gaining a 10/10 convenience, which I think is worth it.
The Python stdlib does not get enough credit. People complain about things like how its http client is dated and slow, but it’s pretty amazing that it’s just right there if you need it, no external dependencies needed. And it’s sitting right next to difflib, graphlib, pathlib, struct, glob, tkinter, and dozens of others. Sure, every one of these is limited individually, but those limitations are stable and well understood!
Currently developing a tkinter app, and loving, that I don't have to install an additional GUI framework. This will be much easier to package than with GTK or QT or Pyside or something. I am sure people have figured out all of that, but my project has so minimal dependencies, and yet offers a full GUI.
tkinter is the best kept secret in the Python std lib.
Pair it with numpy and matplotlib (two external dependencies that personally I consider part of Python itself), and you’ve got 80% of an interactive scientific simulation environment.
I strongly agree! Sure, the stuff in the Python stdlib might not be the best available - but it's always there. I've been in environments where I couldn't install requests but urllib2 meant that I could still get the job done. I think that every language should have a batteries-included stdlib.
Odd, I don't see any mention of subprocess.run, the workhorse of python scripting.
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.
Also `asyncio.subprocess`, which lets you manage multiple concurrently running commands. Very handy if you need to orchestrate several commands together.
I think the point is that for most things, you don't need to call any external tools. Python's standard library comes already with lots of features, and there are many packages you can install.
Pretty much anything longer then a throwaway one liner I write in python.
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.
You can always set shell=True and pass in an entire command line as a string, but… don’t do that. It seems really nice until the first time you get the shell escaping wrong, and then it’s something you tend never to do again.
I have been converting a lot of my makefiles to pyinvoke and fabric and it makes things so much easier to manage than bash or make. Don't know why I held on for so long.
I've never liked shell scripting. Last year, I switched my build system of a Rust project over to Python (Cargo is actually quite limited as a build system). For a newer project, I'm using Rust itself with the XTask pattern. I'm not sure if I prefer the Python or Rust approach yet.
At work my machine has probably ten or more installations of Python hidden in various tools. I'm certainly not alone. So we could say "on average Python is installed on every machine". /s
If a script is simple - I use posix sh + awk, sed, etc.
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).
Because the instant I want a dependency, I’m screwed. Half of what I want isn’t on CPAN, and the other half is ten years out of date and requires a compiler toolchain, fifty undocumented build dependencies at nonstandard paths, and five obscure/package-specific environment variables set in order to install. Python? Python either has it in the stdlib or has wheels available for pretty much everything.
Talking super simple stuff here, too: database drivers, markdown formatters, structured data parsers.
I strongly agree with this.
At $WORK I usually work on projects comprising many small bits in various languages - some PowerShell here, some JS there, along with a "build process" that helps minify each and combine them into the final product.
After switching from shell scripts to Just and having to deal with a ton of issues on the way (how does quoting work in each system? How does argument passing? Environment variables?) I simply wrote a simple script with Python, UV shebang and PEP723 dependencies. Typer takes care of the command line parsing, and each "build target" is a simple, composable, readable python function that takes arguments and can call other ones if it needs to. Can't be simpler than that and the LLMs love it too.
I like the message the article is trying to convey, Python is good alternative to complicated shell scripts in my opinion.
I do wonder, let's say the scripting file is using lots of libraries, do you have to include some kind of requirements.txt file with it aswell when you want to share it with other people?
In Ruby, there is inline bundler which makes sharing a single Ruby script very portable.
I wrote another comment here about a strategy for writing portable Bash scripts without compromising on features and freely using arbitrary external commands. I wanted to give an example from the article of how I'd likely write one of his examples of a "somewhat unreadable shell script" in this style.
His ugly sh example:
morning_greetings=('hi' 'hello' 'good morning')
energetic_morning_greetings=()
for s in "${morning_greetings[@]}"; do
energetic_morning_greetings+=( "${s^^}!" )
done
and his more readable Python equivalent:
morning_greetings = ['hi', 'hello', 'good morning']
energetic_morning_greetings = \
[s.upper() + '!' for s in morning_greetings]
And I'd write the shell version in Bash something like this:
Does it still involve more syntax? Yeah. Printing arrays in Bash always involves some. But it's clearer at a glance what it does, and it doesn't involve mutating variables.
The piece this example is too simple to show (since it's focused only on data operations and not interacting with the filesystem or running external programs) is how much shorter the Bash usually ends up being than the Python equivalent.
/*
Oh, you're looking at my CSS. Here be dragons.
Let's bundle in a rant while you're here: I'd like to use variable width fonts
instead of fixed size fonts. I enjoy 300 (i.e. light) and 600 (i.e. semibold)
over regular and bold for Crimson Pro only. But Chrome doesn't like to show
anything but 400 and 700 with variable width fonts, so I'll have to trick Chrome
by declaring my 300 and 600 fonts as if they were 400 and 700 respectively.
... or well, I could use font-variation-settings, but that's a global override
which means I have to specify it EVERYWHERE and that's just too much effort for
poor me.
*/
Funny this shows up on HN now! Jean Niklas wrote his thesis about RRB trees, which is an evolution of the persistent vectors of clojure fame. I have spent many hours reading his thesis lately, because
I just spent two months porting his c-rrb (https://github.com/hypirion/c-rrb) to c#, which was a fun endeavour. I wish I would have read hus thesis better since I spent two weeks debugging issues that arise from me not enforcing the leftwise dense invariant with regards to the tail.
So they suggest to write scripts in Python rather than shell because Python is stable, probably installed on the target machine, has a big standard library, and is more readable. Many people do so.
That's the bright side of Python. They should mention the dark side, or Why _not_ to use Python for scripting.
First of all, the promise of easy portability breaks as soon as the script has dependencies. Try to install some Python program on a server where you're not root and a minimal python3 is installed.
The stability isn't very good in my experience either. I've often seen programs not compatible with recent releases of Python, either explicitly in the README or implicitly at runtime. Unmaintained Python code breaks.
Unfortunately, there is no silver bullet. Posix shell or bash may be better for simple scripts; Perl or Python if you know you won't require dependencies or if you have a good control on where to install the script; languages that compile to static executables are not really "scripting", but may be a better choice for long(term usage. These past years, I tend to keep away from Python as much as I can.
62 comments
[ 0.22 ms ] story [ 104 ms ] threadC# 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....
You can also add a shebang line so you can execute it directly:
Then:If 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.
The same way you handle them with bash?
Install them?
What are we talking about here?
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.
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.
Python packages are fine for servers but not for CLI tools.
The sqlite, tkinter, and shelve modules are the ones I find most impressive.
Pair it with numpy and matplotlib (two external dependencies that personally I consider part of Python itself), and you’ve got 80% of an interactive scientific simulation environment.
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:
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.https://pypi.org/project/sh/
Basically you can just `from sh import [command]` and then have an installed binary command available as function
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,
and have significant different behavior.> 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.
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
[1] - https://github.com/enthus1ast/zigcc
Python:
Talking super simple stuff here, too: database drivers, markdown formatters, structured data parsers.
I do wonder, let's say the scripting file is using lots of libraries, do you have to include some kind of requirements.txt file with it aswell when you want to share it with other people?
In Ruby, there is inline bundler which makes sharing a single Ruby script very portable.
https://bundler.io/guides/bundler_in_a_single_file_ruby_scri...
His ugly sh example:
and his more readable Python equivalent: And I'd write the shell version in Bash something like this: Does it still involve more syntax? Yeah. Printing arrays in Bash always involves some. But it's clearer at a glance what it does, and it doesn't involve mutating variables.The piece this example is too simple to show (since it's focused only on data operations and not interacting with the filesystem or running external programs) is how much shorter the Bash usually ends up being than the Python equivalent.
That's the bright side of Python. They should mention the dark side, or Why _not_ to use Python for scripting.
First of all, the promise of easy portability breaks as soon as the script has dependencies. Try to install some Python program on a server where you're not root and a minimal python3 is installed.
The stability isn't very good in my experience either. I've often seen programs not compatible with recent releases of Python, either explicitly in the README or implicitly at runtime. Unmaintained Python code breaks.
Unfortunately, there is no silver bullet. Posix shell or bash may be better for simple scripts; Perl or Python if you know you won't require dependencies or if you have a good control on where to install the script; languages that compile to static executables are not really "scripting", but may be a better choice for long(term usage. These past years, I tend to keep away from Python as much as I can.