Add a command-line interface to the secrets module

The secrets module currently has no functional command-line interface. Running python -m secrets produces no output.

I propose adding a small CLI for generating cryptographically secure tokens:


python -m secrets
python -m secrets --hex 32
python -m secrets --urlsafe 32
python -m secrets --count 5

Proposed behavior:

  • Generate a URL-safe token by default.
  • Support URL-safe and hexadecimal output.
  • Allow the number of random bytes to be specified.
  • Allow multiple tokens to be generated, one per line.
  • Reject invalid sizes, counts, and incompatible options with a nonzero exit status.
  • Use the module’s existing default entropy when no size is specified, rather than duplicating that value in the CLI.

For example:

```bash
$ python -m secrets --hex 16 --count 2
e0a67e40a9f36b31a86f733d46b83d12
522231184e0b7d012c27ace14d28fb86

```

Currently, users need an inline Python expression:

python -c “import secrets; print(secrets.token_urlsafe(32))”

A dedicated CLI would be shorter, easier to remember, less error-prone, and more convenient in shell scripts.

Several standard-library modules already provide useful python -m interfaces, including uuid, random, json, and base64. Secure token generation is also a common command-line task, so exposing this existing functionality through python -m secrets would improve consistency.

It would additionally provide automation and coding agents with a constrained command instead of requiring them to construct and escape arbitrary Python source for python -c.

If this approach seems appropriate, I would be interested in implementing it, including tests and documentation.

One design question is whether the byte count should be positional or explicit; there could be several other design decisions.

python -m secrets --hex 32 vs python -m secrets --hex --bytes 32

6 Likes

Maybe the CLI would want generate and check subcommands, the latter eg to wrap secrets.compare_digest.

I made WerkzeugSecurityCLI (PyPI) as an (unofficial) wrapper around werkzeug’s password hashing functionality which provides wzscli(1) with wzscli generate and wzscli check subcommands. I needed a way to create password hashes on the command line for inserting into sqlite databases for testing.

I like the idea of a simple token generation CLI (with no subcommand needed)

Exposing a check subcommand (mapping to compare_digest) also seems reasonable, but I don’t think it needs to be at the cost of requiring a named subcommand for the token generation API.

3 Likes

Thanks @ncoghlan and @ncoghlan fair enough.

I needed a way to create password hashes on the command line for inserting into sqlite databases for testing.

Me too, I need it now and then for several other tasks.

Do you have any real evidence that this is the case? It’s shorter, yes. But is it easier to remember? I find it hard to remember which stdlib modules have command line interfaces. I’d accept less error prone if there were string arguments (because nested quoting is a pain) but not for numbers. And how is it more convenient in a shell script? Having a bit more to type is irrelevant in a shell script.

Also, a command line interface restricts portability - your code is now limited to Python 3.16+, whereas if you use -c it works on Python 3.6+.

I don’t see any point in wrapping compare_digest. It’s literally just an equality comparison. If you’re concerned about timing attacks, you shouldn’t really be using a command line utility anyway.

1 Like

I mean memorability is not really the main point here. people will generate a secret once and configure it somewhere like github actions or a test instance, they are not running it every day.

the real thing is when you are in a constrained env, a bare server or a ci job, python -m secrets is discoverable, you just do --help and you are done. with python -c you already need to know the function name and the exact syntax, and quoting in shell scripts or yaml is just painful.

also we already have python -m uuid, python -m random, python -m base64 etc. so its just consistency with what stdlib already does

I mean using it as python3 -m secret

You have to at least know that the secrets module is what you want, though. And if you have that, chances are you know what function within it you want.

I’d be much more inclined towards a fully generic way to call on a specific function from a specific module, rather than ad-hoc CLIs being added to different modules without any consistency. Why does this module get one and not that module? What’s the syntax for each different module? I’m not really sure it’s any more discoverable than python -c. Yes, it’s a bit less typing, and if you’re very familiar with this particular module’s CLI, it can be handy. But I don’t think every module needs this sort of thing.

Quick quiz: Which modules currently have python -m modulename CLIs?

You don’t know? Neither do I. And I have no idea how I would go about finding that list, other than trying them all one by one, or looking at the docs - and if I’m doing the latter, it’s not that much harder to find a specific function and use python -c.

2 Likes

I think this is a misguided argument in almost any context. If the CLI is a good idea, then we need to introduce it now so that in 10 years it is portable. If it isn’t a good idea, we shouldn’t introduce it and don’t have to worry about compatibility. This argument can be used to completely stop development of python and only do bugfixes. Clearly we don’t want to take that decision, so the argument is pointless,.

We need to design for the future, not for now. Yes, we need to make sure it works in the interim (especially with packaging), but we can’t let the current situation keep us from imagining a better future.

This isn’t too say this CLI is clearly a good idea - I just don’t like this particular argument I have seen a few time in related contexts. The somewhat related argument that doing nothing wins by default does however work ofcourse.


Relatedly: Ideally the answer is “any module that has a clear functionality that can be exposed as a CLI” :stuck_out_tongue:

More seriously, I don’t think this is a useful way to look at this. In an actual situation, noone goes this direction, i.e. searching for modules to use. Instead they have a usecase and then check if the module has a CLI. (e.g. by running python -m module and seeing what happens) This isn’t something that you need to need to know of the top of your head, it’s just something you need to be able to lookup.

It’s not like you can name all builtin functions of the top of your head, or all stdlib modules.

Although I still think a generic way to call most top level functions in the python stdlib is potentially a useful tool, if an intuitive and clear spelling can be found.

9 Likes

If that IS the ideal answer, then someone should go through ALL modules and make a coherent and consistent CLI for each one that makes sense, and make a positive decision about which ones won’t. Otherwise it will continue to be ad-hoc, and there won’t be any logic to (a) which ones have CLIs or (b) what the arguments are.

That’s true, but I can load up the REPL and type “import ” to get a list of them all. How do I get a list of all the modules with CLIs? That was my point: it’s not really any more discoverable if there’s no way to discover it.

I’d be fine if it’s technically able to call anything but with limitations, eg only string arguments can be passed and only the repr will be output. So you could do something like python3 -r requests.get https://some.url.example/whatever but all you would get back is <Response [200]>, making that example not very useful. However, python3 -r base64.b64decode HelloPythonWorld would print out b'\x1d\xe9e\xa0\xfc\xad\x86\x89\xd6\xa2\xb9]'. That would be viable for a lot of simple functions. Also, python3 -r base64.b64decode --help could do help(base64.b64decode).

But again, this is not about discoverability. It’s JUST a convenience for certain common cases. For everything else, use python3 -c blah blah. Though… just tossing this out there - what if python3 -ic 'base64.b64encode(b"asdf")' did an interactive evaluation, automatically importing stdlib modules as needed, and printing out the result? That’d reduce the hassles of -c a bit.

Why would you want to? That was my point: Sure, this is currently difficult, but I don’t see the point as long as it’s easy to check if a specific module has a CLI. It’s not like there is an easy way to check all stdlib modules that define a function that starts with a. There is an intellectual curiosity to fill here, but I don’t believe (and you haven’t argued for) that there is an actually partical usecase for that.

And creating a help topic that lists all official CLIs would not be difficult. It would just be a list of names with short descriptions that, yes, would need to kept in sync manually, but they change rarely. If this is something you really miss, creating it would not be difficult.


FWIW, I don’t think printing the repr is particularly useful. Sure, for b64decode raw bytes might be a weird output, but for b64encode the useful things to do would absolutely to just print the result, and adding the quotation marks would just make the interface close to unusable. Anyway, that’s offtopic for this thread which is about the secrets module.

4 Likes

Just for what it’s worth, this page now exists: Modules command-line interface (CLI) — Python 3.14.5 documentation

It seems to me that every time there’s a discussion about adding a CLI to the standard library, it eventually circles back to reinventing pyp :slight_smile:

4 Likes

Okay, so why do people claim that a CLI is more discoverable than python3 -c then?

Noone in this thread has claimed that AFAICT, and I definitely haven’t. I don’t think it’s true as an absolute statement. The module you need to use is equally discoverable. But that’s only half the story. Just because you know what module you need to use doesn’t mean you know exactly what functions in there you need to use in what way. Having the common code paths be exposed in the CLI makes it easy to “discover”.

(not that it really matters, because according to your logic the fact that the documentation page exists disarms your argument anyway. I am probably going to stop responding if your continue on this tangent)

I’m talking about things like this:

So what IS the advantage of python -m secrets over python -c 'import secrets; secrets...' then? I’m at a loss.

1 Like

It seems pretty obvious to me that typing python -m secrets into the terminal is an easy step to take. No need to read any documentation. It either works, or doesn’t. No danger, no real time lost.

In contrast:

python -c 'import secrets; secrets...'
  File "<string>", line 1
    import secrets; secrets...
                    ^^^^^^^
SyntaxError: invalid syntax. Did you mean 'assert'?

this is going to be an absolute pain to debug. I would probably ask a chatbot, and it would probably fix it for me, but that’s still 10x the time investment (for me as a user who doesn’t know the ‘secrets’ module) to figure out how this works if it works.

1 Like
>>> import random
>>> random.<tab><tab>

I can see randint there. That looks like a good start.

>>> random.randint(10)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    random.randint(10)
    ~~~~~~~~~~~~~~^^^^
TypeError: Random.randint() missing 1 required positional argument: 'b'
>>> random.randint(1, 10)
8

Can’t really say that I have a lot of difficulty with that particular example. (Knowing what I know, I would actually use randrange more often than randint, but randint is a more obvious choice in the tab completion list.)

If you’re going to assume that python -m is used interactively, it’s only fair that discovery of functions to use with python -c can be done using the REPL. It’s there for a reason.

1 Like

In Python 3.13 and earlier, the error message is clear:

python3.13 -c 'import secrets; secrets...'
  File "<string>", line 1
    import secrets; secrets...
                           ^^^
SyntaxError: invalid syntax

You only need to know that there’s a module called secrets that generates a random secret of whatever length you specify.

Do we need one if we are using the terminal? How many existing methods are there for generating a secure random string in the terminal?

Please don’t put words in my mouth. All I’m saying is that the case for this is pretty weak. And I never said anything about a security vulnerability.

I’m dropping out of the discussion at this point. I don’t see any reason to try to debate any further if this is your response.