If I read your question correctly, you want to create some kind of arbitrary identifier token, which should be 21 characters max. Do I need to be very resistant to guessing? The example you gave is not “critographically strong” in the sense that it can be guessed by looking for less than 1/2 of the total possible key space.
You do not say whether all characters can be 256 ASCII characters, or if they should be limited to, say, printable ASCII (33-127, inclusive) or a smaller range.
There is a Python module designed for UUIDs (universal unique identifiers). You probably want uuid4 to generate a random UUID and use OS support if available (on Linux, Mac, FreeBSD, and possibly others).
>>> import uuid >>> u = uuid.uuid4() >>> u UUID('d94303e7-1be4-49ef-92f2-472bc4b4286d') >>> u.bytes '\xd9C\x03\xe7\x1b\xe4I\xef\x92\xf2G+\xc4\xb4(m' >>> len(u.bytes) 16 >>>
16 random bytes are very unobvious, and there is no need to use the full 21 bytes that your API allows if all you want to have an indescribable opaque identifier.
If you cannot use such raw bytes, this is probably a bad idea, because it’s harder to use debugging in logs and other messages, and it’s harder to compare by eye and then convert the bytes to something more readable, for example using base-64 encoding, with the result reduced to 21 (or any other) bytes:
>>> u.bytes.encode("base64") '2UMD5xvkSe+S8kcrxLQobQ==\n' >>> len(u.bytes.encode("base64")) 25 >>> u.bytes.encode("base64")[:21] '2UMD5xvkSe+S8kcrxLQob' >>>
This gives you an extremely high-quality random string of length 21.
You may not like the “+” or “/” character, which can be in the base-64 string, because without proper escaping, which can interfere with working with URLs. Since you already thought of using “random 3 characters”, I don’t think it bothers you. If so, you can replace these characters with something else ("-" and "." May work) or delete them, if any.
As others have pointed out, you can use .encode ("hex") and get the hexadecimal equivalent, but only 4 bits of randomness / character * 21 max characters gives you 84 bits of randomness, not twice. Each bit doubles your key space, making the theoretical search space much, much smaller. 2E24 less.
Your keyspace is still 2E24 in size, even with hexadecimal encoding, so I think this is a more theoretical problem. I would not worry about people making brutal attacks against your system.
Edit
PS: The uuid.uuid4 function uses libuuid, if available. This gets its entropy from os.urandom (if available) otherwise from the current time and the local ethernet MAC address. If libuuid is not available, the uuid.uuid4 function receives bytes directly from os.urandom (if available), otherwise it uses a random module. The random module uses an initial default value based on os.urandom (if available), otherwise a value based on the current time. Breakdown occurs for each function call, so if you do not have os.urandom, then the overhead is a little more than you might expect.
Take a home message? If you know that you have os.urandom, you can do
os.urandom(16).encode("base64")[:21]
but if you do not want to worry about its availability, use the uuid module.