Random uppercase letter generator

Uppercase letters only, drawn uniformly from 26 characters. Each one carries 4.700440 bits.

G
More options
up to 10,000

Constraints

26 characters, 4.700440 bits each

log2(26) is 4.700440, so eight uppercase letters carry 37.60 bits and twelve carry 56.41. Both of those sit in the range NIST SP 800-63B-4 says must be rate-limited: it treats any secret below 64 bits as one that cannot stand on its own. That is not a criticism of the alphabet, it is a statement about length. Fourteen characters reaches 65.81 bits and clears the floor.

Twenty-six is not a power of two, so a correct generator has to reject and redraw. 2 to the 32 divided by 26 leaves a remainder of 22, so twenty-two values out of 4,294,967,296 are discarded and redrawn. The bias from skipping that step is small at this alphabet size but it is not zero, and there is no reason to accept it when the fix costs one comparison.

Uppercase-only lengths and what they carry
CharactersBitsBandTypical use
418.80BrokenA code that only works behind strict rate limiting
628.20BrokenShort reference codes, printed or read aloud
837.60WeakCoupon and voucher codes with server-side limits
1465.81ModerateA secret that needs no rate limiting
1884.61StrongLong-lived identifiers
28131.61CryptographicKey-equivalent material in letters only

Uppercase-only removes two confusions and leaves one

Most look-alike problems are cross-class. Zero against capital O and one against capital I are digit-versus-letter confusions, and lowercase l against capital I and against digit one is the classic three-way clash. An alphabet with no digits and no lowercase letters cannot have any of them, which is why uppercase-only codes are the traditional choice for anything printed on a card or read down a phone line.

What remains is digram confusion, and it cannot be fixed by removing characters. VV reads as W. RN reads as M in many typefaces. The Unicode confusables data, UTS #39 version 17.0.0, contains exactly seven ASCII-to-ASCII confusable mappings, and the multi-character cases among them survive any amount of alphabet pruning. Any tool claiming to remove all ambiguity is overclaiming, and this one does not make that claim.

If the code will be printed, the typeface matters as much as the alphabet. VV against W and RN against M are properties of the glyphs, not of the character set.

Case-insensitive by construction

An uppercase-only value loses nothing when it passes through something that folds case. That is a longer list than it first appears: DNS labels are case-insensitive by specification, HTTP header field names are too, Windows file names and the default macOS APFS configuration are case-insensitive, MySQL ships a case-insensitive collation by default, and many form fields and scanners uppercase their input on the way in.

A mixed-case token going through any of those silently loses a bit per character while still looking correct. An uppercase-only token cannot, because there is nothing to fold. The entropy shown on screen is the entropy that survives the round trip, which is the whole reason to choose this alphabet over a larger one.

The one bit that adding lowercase buys

log2(52) equals log2(26) plus 1, exactly, because 52 is twice 26. Adding lowercase letters to an uppercase alphabet buys precisely one bit per character, no more and no less. Eight characters goes from 37.60 bits to 45.60. It is the cleanest trade in the whole cluster and it comes with one condition: everything that touches the value has to preserve case, or the bit is not there.

Adding digits instead takes 26 characters to 36 and buys 0.469 bits per character, which is less. Adding both takes it to 62 and buys 1.254. The decision is not really about entropy at these sizes, it is about which downstream systems you trust with case.

Doing this in your own code

JavaScript
// Uniform over the alphabet, with no modulo bias.
// The alphabet is spread into code points first, so an emoji or an
// accented character counts as one symbol rather than two UTF-16 units.
function randomString(length, alphabetStr) {
  const alphabet = [...new Set([...alphabetStr])];
  const n = alphabet.length;
  if (n < 1) throw new RangeError("empty alphabet");

  const limit = 4294967296 - (4294967296 % n);   // largest multiple of n <= 2^32
  const buf = new Uint32Array(1);
  let out = "";

  for (let i = 0; i < length; i++) {
    let x;
    do { crypto.getRandomValues(buf); x = buf[0]; } while (x >= limit);
    out += alphabet[x % n];
  }
  return out;
}

randomString(16, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");

The do/while is the whole point. Dropping it in favour of a bare x % n reintroduces modulo bias.

Python 3.6+
import secrets

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

"".join(secrets.choice(ALPHABET) for _ in range(16))

# secrets.choice is backed by secrets.randbelow, which rejects and redraws,
# so it is uniform. random.choice is not a secure source and must not be used.

Iterating a Python str yields code points, so non-ASCII alphabets work without extra handling.

Go
import (
    "crypto/rand"
    "math/big"
)

func RandomString(length int, alphabet string) (string, error) {
    runes := []rune(alphabet)
    n := big.NewInt(int64(len(runes)))
    out := make([]rune, length)
    for i := range out {
        j, err := rand.Int(rand.Reader, n)   // rejection sampled internally
        if err != nil {
            return "", err
        }
        out[i] = runes[j.Int64()]
    }
    return string(out), nil
}

Convert to []rune, not []byte. Indexing a Go string yields bytes, which splits any multi-byte character.

Rust
// rand = "0.9"
use rand::Rng;

const ALPHABET: &[u8] =
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

fn random_string(length: usize) -> String {
    // rand::rng() is a thread-local ChaCha12 CSPRNG seeded from the OS.
    let mut rng = rand::rng();
    (0..length)
        .map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char)
        .collect()
}

random_range rejection-samples, so it is unbiased. On rand 0.8 the method is named gen_range and the constructor is rand::thread_rng().

PHP 7+
<?php
function random_string(int $length, string $alphabet): string {
    $n = strlen($alphabet);
    $out = '';
    for ($i = 0; $i < $length; $i++) {
        $out .= $alphabet[random_int(0, $n - 1)];
    }
    return $out;
}

echo random_string(16, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');

random_int throws on entropy failure and is unbiased. rand and mt_rand are neither.

Java 8+
import java.security.SecureRandom;

static String randomString(int length, String alphabet) {
    SecureRandom rng = new SecureRandom();
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
        sb.append(alphabet.charAt(rng.nextInt(alphabet.length())));
    }
    return sb.toString();
}

nextInt(bound) rejects and redraws, so it is uniform. Reuse one SecureRandom instance rather than constructing one per call.

C# / .NET
using System.Security.Cryptography;

const string Alphabet =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

// .NET 8 and later
string s = RandomNumberGenerator.GetString(Alphabet, 16);

// Earlier versions, same distribution
var sb = new System.Text.StringBuilder(16);
for (int i = 0; i < 16; i++)
    sb.Append(Alphabet[RandomNumberGenerator.GetInt32(Alphabet.Length)]);
Shell
# 16 alphanumeric characters from the kernel CSPRNG.
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16; echo

# tr -dc discards every byte outside the set rather than folding it in,
# which makes it a rejection sampler. Each of the 62 bytes has exactly one
# preimage, so the output is uniform.

# openssl, when you want bytes rather than a character set
openssl rand -hex 16
openssl rand -base64 24

Questions

How much entropy is in eight random uppercase letters?

37.60 bits, from 8 times log2(26). That is 208,827,064,576 possible values. NIST SP 800-63B-4 treats anything below 64 bits as needing rate limiting, so eight letters is fine as a coupon or reference code with server-side limits and unsuitable as a standalone secret.

Why exclude digits from a code that people will read?

It removes the zero-against-O and one-against-I confusions entirely rather than patching them. The cost is 0.469 bits per character against a 36-character alphabet. If you want digits and unambiguity together, use Crockford Base32, which keeps the digits and drops I, L, O and U instead.

Are uppercase-only codes weaker because there are fewer characters?

Per character, yes, and length compensates linearly. Fourteen uppercase letters carry 65.81 bits, more than eleven mixed-case alphanumerics at 65.50. Choosing a smaller alphabet is only a mistake if you keep the length fixed while doing it.

Does this generator ever produce a real word?

It can, and at eight characters it is rare but not impossible, since every string of that length is equally likely. If accidental words or offensive strings are a business risk, the nolookalikesSafe alphabet is vowel-free by design and is available on the custom character set page.

Is A more likely than Z?

No. The sampler rejects and redraws rather than folding the remainder back in, so all 26 letters are exactly equally likely. A naive implementation using a byte and a modulo would make the first four letters more likely, since 256 is 9 times 26 plus 22.

Related generators