Skip to content
Comparisons

Pseudo Random Code Generator in Python, Java, and JS

By The EbookWale Team · Updated August 20, 2026 · 22 min read

Build a seeded pseudo random code generator in Python, Java, and JavaScript, verify its output, and choose secure APIs when codes must be secret.

A pseudo random code generator uses a deterministic number generator, then maps its numeric output to characters such as letters and digits. This guide builds the same seeded generator in Python, Java, and JavaScript, verifies all three against one test vector, and replaces it with secure language APIs when predictability would be dangerous.

A pseudo random code generator can reproduce the same character sequence from the same seed only when its algorithm, initial state, and ordered calls are also identical.

What a Pseudo Random Code Generator Actually Does

A pseudorandom number generator, usually shortened to PRNG, is an algorithm that maintains an internal state and updates that state each time a value is requested. The initial state is commonly derived from a seed.

The output looks irregular, but the process is deterministic. Deterministic means that the same calculation starts from the same state and produces the same next state. If two implementations agree on every arithmetic rule, they can replay the same sequence exactly.

The word pseudorandom distinguishes this calculated sequence from values obtained directly from an unpredictable physical process. A normal PRNG aims to produce values that are useful for tasks such as simulations, games, randomized tests, and repeatable sample data. It has a finite number of possible states, so its sequence must eventually repeat.

A code generator adds a formatting step:

  1. Ask the PRNG for a number.
  2. reduce that number to a valid character position.
  3. Select the character at that position.
  4. Repeat until the code reaches the requested length.

For an alphabet such as ABC...XYZ012...789, an index of 0 selects A, while an index of 25 selects Z. The PRNG generates numbers; the code generator decides what those numbers mean.

One value follows two pathsCurrentstateLCGmathNew stateand outputsaved for the next calloutput pathboundedindexcharacter
State evolves in a loop; code formatting interprets each output.

That distinction matters during debugging. A wrong number indicates a state or arithmetic error. Correct numbers paired with wrong characters indicate a range conversion or alphabet error.

Trace a Small Generator by Hand

A linear congruential generator, or LCG, updates its state with this recurrence:

X[n+1] = (a * X[n] + c) mod m

The four inputs have separate jobs:

SymbolNamePurpose
X[n]Current stateThe value carried from the previous call
aMultiplierMultiplies the current state
cIncrementAdds an offset before reduction
mModulusLimits every state to a fixed range
X[0]SeedSupplies the initial state

Consider a deliberately tiny example with a = 5, c = 3, m = 16, and seed X[0] = 7.

The first update is:

X[1] = (5 * 7 + 3) mod 16
     = 38 mod 16
     = 6

Continue by feeding each result back into the same formula:

X[2] = (5 * 6 + 3) mod 16 = 33 mod 16 = 1
X[3] = (5 * 1 + 3) mod 16 = 8
X[4] = (5 * 8 + 3) mod 16 = 43 mod 16 = 11
X[5] = (5 * 11 + 3) mod 16 = 58 mod 16 = 10

The sequence begins 6, 1, 8, 11, 10. Nothing is chosen during those calculations. The apparent variation comes from repeatedly updating the state and wrapping the result with the modulus.

This small generator is useful for learning because every calculation fits on paper. It is not suitable for production. Small parameters provide few possible states and make repetition easy to observe. Parameter selection also affects the period, which is the number of updates before a state repeats.

🔑 REMEMBER —

The seed is the first state, not a permanent input to every call. Each call must store its new state before the following call begins.

The implementations below use the same recurrence with a 32-bit state:

X[n+1] = (1664525 * X[n] + 1013904223) mod 2^32

This remains a teaching generator. Its value here is portability and transparent arithmetic, not statistical strength or security.

Three runtimes, one recurrencePythonmask to32 bitsJavalong maththen maskJavaScriptimul thenunsignedidentical unsigned32-bit statessame seed + same calls = same vector
Different numeric tools preserve one shared 32-bit calculation.

Build the Same Generator in Python, Java, and JavaScript

All three versions below implement the same operations:

  • Store an unsigned 32-bit state.
  • Update it with the specified LCG recurrence.
  • Produce an unbiased bounded integer by rejecting values outside an evenly divisible range.
  • Build a code from uppercase letters, lowercase letters, and digits.
  • Check the first five values from seed 42.

The fixed test vector is:

1083814273,378494188,2479403867,955863294,1613448261

Each listing is a complete, standalone implementation. They are equivalents, so they should be run separately rather than concatenated.

Python implementation

Python integers do not overflow automatically. Masking with 0xFFFFFFFF keeps the state within the intended unsigned 32-bit range.

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
UINT32_SIZE = 1 << 32


class Lcg32:
    def __init__(self, seed):
        self.state = seed & 0xFFFFFFFF

    def next_u32(self):
        self.state = (
            1664525 * self.state + 1013904223
        ) & 0xFFFFFFFF
        return self.state

    def next_int(self, bound):
        if not isinstance(bound, int) or bound <= 0 or bound > UINT32_SIZE:
            raise ValueError("bound must be between 1 and 2^32")

        limit = (UINT32_SIZE // bound) * bound
        while True:
            value = self.next_u32()
            if value < limit:
                return value % bound

    def code(self, length):
        if not isinstance(length, int) or length < 0:
            raise ValueError("length must be a non-negative integer")

        return "".join(
            ALPHABET[self.next_int(len(ALPHABET))]
            for _ in range(length)
        )


expected = [
    1083814273,
    378494188,
    2479403867,
    955863294,
    1613448261,
]

generator = Lcg32(42)
actual = [generator.next_u32() for _ in expected]

if actual != expected:
    raise AssertionError(f"expected {expected}, got {actual}")

code_generator = Lcg32(42)
generated_code = code_generator.code(5)
if generated_code != "X874D":
    raise AssertionError(f"expected X874D, got {generated_code}")

print("test vector passed")
test vector passed

Java implementation

Java’s long type gives the multiplication enough room for this calculation. The mask converts the stored result back to an unsigned 32-bit value represented inside a long.

public final class Lcg32 {
    private static final String ALPHABET =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private static final long UINT32_SIZE = 1L << 32;

    private long state;

    public Lcg32(long seed) {
        this.state = seed & 0xFFFFFFFFL;
    }

    public long nextUint32() {
        state = (1664525L * state + 1013904223L) & 0xFFFFFFFFL;
        return state;
    }

    public int nextInt(int bound) {
        if (bound <= 0) {
            throw new IllegalArgumentException(
                "bound must be a positive integer"
            );
        }

        long limit = (UINT32_SIZE / bound) * bound;
        long value;

        do {
            value = nextUint32();
        } while (value >= limit);

        return (int) (value % bound);
    }

    public String code(int length) {
        if (length < 0) {
            throw new IllegalArgumentException(
                "length must be non-negative"
            );
        }

        StringBuilder result = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            result.append(ALPHABET.charAt(nextInt(ALPHABET.length())));
        }
        return result.toString();
    }

    public static void main(String[] args) {
        long[] expected = {
            1083814273L,
            378494188L,
            2479403867L,
            955863294L,
            1613448261L
        };

        Lcg32 generator = new Lcg32(42);

        for (long expectedValue : expected) {
            long actualValue = generator.nextUint32();
            if (actualValue != expectedValue) {
                throw new AssertionError(
                    "expected " + expectedValue + ", got " + actualValue
                );
            }
        }

        Lcg32 codeGenerator = new Lcg32(42);
        String generatedCode = codeGenerator.code(5);
        if (!generatedCode.equals("X874D")) {
            throw new AssertionError(
                "expected X874D, got " + generatedCode
            );
        }

        System.out.println("test vector passed");
    }
}
test vector passed

JavaScript implementation

JavaScript uses floating-point numbers for ordinary numeric values. Math.imul performs the multiplication with 32-bit integer semantics, and >>> 0 converts the result to an unsigned 32-bit value.

const ALPHABET =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const UINT32_SIZE = 0x100000000;

class Lcg32 {
  constructor(seed) {
    if (!Number.isSafeInteger(seed)) {
      throw new TypeError("seed must be a safe integer");
    }

    this.state = seed >>> 0;
  }

  nextUint32() {
    this.state =
      (Math.imul(1664525, this.state) + 1013904223) >>> 0;
    return this.state;
  }

  nextInt(bound) {
    if (
      !Number.isSafeInteger(bound) ||
      bound <= 0 ||
      bound > UINT32_SIZE
    ) {
      throw new RangeError(
        "bound must be a positive integer no greater than 2^32"
      );
    }

    const limit = Math.floor(UINT32_SIZE / bound) * bound;

    while (true) {
      const value = this.nextUint32();
      if (value < limit) {
        return value % bound;
      }
    }
  }

  code(length) {
    if (!Number.isSafeInteger(length) || length < 0) {
      throw new RangeError(
        "length must be a non-negative safe integer"
      );
    }

    let result = "";
    for (let i = 0; i < length; i += 1) {
      result += ALPHABET[this.nextInt(ALPHABET.length)];
    }
    return result;
  }
}

const expected = [
  1083814273,
  378494188,
  2479403867,
  955863294,
  1613448261,
];

const generator = new Lcg32(42);
const actual = expected.map(() => generator.nextUint32());

if (actual.join(",") !== expected.join(",")) {
  throw new Error(`expected ${expected}, got ${actual}`);
}

const codeGenerator = new Lcg32(42);
const generatedCode = codeGenerator.code(5);
if (generatedCode !== "X874D") {
  throw new Error(`expected X874D, got ${generatedCode}`);
}

console.log("test vector passed");
test vector passed

The test vector catches several common porting errors. A signed JavaScript result, an omitted Python mask, a signed-versus-unsigned Java comparison, or an incorrect state update will change the sequence. Each entry point then creates a fresh generator before checking the five-character code X874D, because the vector has already advanced the first generator by five calls.

Seeds, State, and Reproducible Results

A reproducible run requires three things to stay fixed:

  1. The exact generator algorithm and arithmetic rules.
  2. The initial state derived from the seed.
  3. The number and order of generator calls.

Suppose a randomized test fails after generating twelve values. Saving the seed is helpful, but it is not enough if a later edit inserts an extra random call before the failure. That added call advances the state, so every following value changes.

Call order is part of the replaysame seedoriginal runedited runwidth uses S1height uses S2label uses S3width uses S1extra calluses S2height uses S3label uses S4replaymatchesresults shifted
One inserted call shifts the state used by every later operation.

A reliable replay records the seed and preserves the call path:

def run_case(seed):
    generator = Lcg32(seed)

    width = generator.next_int(50) + 1
    height = generator.next_int(50) + 1
    label = generator.code(8)

    return {"width": width, "height": height, "label": label}


failed_seed = 812733
first_run = run_case(failed_seed)
replayed_run = run_case(failed_seed)

assert first_run == replayed_run

This example creates a fresh generator for each run. Reseeding an existing generator before every value would repeatedly restart the sequence and damage the intended distribution.

Built-in generators should not be expected to match across languages. Python can reproduce sequences when its seed and call order are controlled. Java Random instances given the same seed and the same sequence of method calls produce matching sequences with one another. JavaScript Math.random() does not let callers choose or reset its seed.

Those facts do not imply that Python, Java, and JavaScript built-ins share an algorithm. Use an explicitly specified generator, such as the teaching implementation above, when a test fixture must be identical across all three languages.

For broader language tradeoffs, the differences between JavaScript and Python and between Java and Python help explain why identical source-level intentions can require different numeric handling.

Turn PRNG Output Into Numbers and Codes

A bounded integer is an integer from 0 up to, but excluding, a chosen bound. For example, next_int(10) returns one of the ten values from 0 through 9.

The tempting conversion is:

raw_value mod bound

That conversion can introduce modulo bias. Bias occurs when the raw generator’s range cannot be split into equal groups for every possible result.

Imagine a raw generator with ten possible outputs, 0 through 9, and a desired bound of 4. Applying % 4 produces:

ResultRaw values that map to itCount
00, 4, 83
11, 5, 93
22, 62
33, 72

The first two results occur more often. Rejection sampling fixes this by accepting only a prefix of the raw range whose size divides evenly by the bound. In this example, it would accept 0 through 7 and retry after 8 or 9.

Where modulo bias comes fromdirect % 4reject, then % 4012301238 and 9 retry3, 3, 2, 2 chances
Rejection sampling removes the leftover values that create modulo bias.

The next_int methods in all three implementations use that method. Their code methods call next_int(ALPHABET.length) once per character, so every alphabet position receives an equal share of the accepted raw values.

A generated code can still collide with a previous code. Random-looking and unique describe different properties:

  • Random-looking code: Its characters are selected from generator output.
  • Unpredictable code: An observer should not be able to guess future values.
  • Unique code: No two issued records are allowed to contain the same value.

A seeded LCG provides reproducibility, not unpredictability. Even a secure generator does not guarantee uniqueness. If duplicates are unacceptable, store issued codes behind a unique database constraint and generate a replacement when insertion reports a collision.

Three different promisesLooks randomgeneratoroutputHard to predictsecure APIrequiredonecodeNo duplicatesunique databaseconstraint
One generated code can require three independent guarantees.
⚠️ GOTCHA —

Do not use the teaching LCG for passwords, login links, verification codes, session values, or access tokens. Anyone who recovers or guesses its state can calculate later outputs.

Use the Language API for Real Programs

Choose an API by task rather than by language alone.

TaskPythonJavaJavaScript
Reproducible tests or simulationsrandom.Random(seed)new Random(seed)A specified seeded generator
Ordinary unseeded behaviorrandomRandomMath.random()
Passwords, tokens, or verification codessecretsSecureRandomCrypto.getRandomValues()

Python’s random module is designed for modeling and simulation rather than security-sensitive values. A dedicated random.Random(seed) instance keeps a test’s state separate from unrelated calls elsewhere in a program. Python’s randrange() also handles bounded values without relying on the older uneven int(random() * n) conversion.

For adversary-facing codes, use secrets.choice:

import secrets
import string

SECURE_ALPHABET = string.ascii_letters + string.digits


def secure_code(length):
    if not isinstance(length, int) or length < 0:
        raise ValueError("length must be a non-negative integer")

    return "".join(
        secrets.choice(SECURE_ALPHABET)
        for _ in range(length)
    )

Java’s Random supports reproducible sequences when instances receive the same seed and the same ordered method calls. It is not suitable for secrets. Use SecureRandom and its bounded method instead:

import java.security.SecureRandom;

public final class SecureCodes {
    private static final String ALPHABET =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private static final SecureRandom RANDOM = new SecureRandom();

    public static String secureCode(int length) {
        if (length < 0) {
            throw new IllegalArgumentException(
                "length must be non-negative"
            );
        }

        StringBuilder result = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            int index = RANDOM.nextInt(ALPHABET.length());
            result.append(ALPHABET.charAt(index));
        }
        return result.toString();
    }
}

JavaScript’s Math.random() returns an approximately uniform value from 0 inclusive to 1 exclusive. It is suitable for ordinary randomized behavior, but callers cannot seed it and should not use it for secrets.

Browser code can use Crypto.getRandomValues(). The following standalone secure alternative uses rejection sampling because the 62-character alphabet does not divide the byte range evenly:

const SECURE_ALPHABET =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

function secureCode(length) {
  if (!Number.isSafeInteger(length) || length < 0) {
    throw new RangeError(
      "length must be a non-negative safe integer"
    );
  }

  const result = [];
  const acceptedRange =
    256 - (256 % SECURE_ALPHABET.length);
  const buffer = new Uint8Array(32);

  while (result.length < length) {
    globalThis.crypto.getRandomValues(buffer);

    for (const value of buffer) {
      if (value < acceptedRange) {
        result.push(
          SECURE_ALPHABET[value % SECURE_ALPHABET.length]
        );

        if (result.length === length) {
          break;
        }
      }
    }
  }

  return result.join("");
}

The practical dividing line is simple. Use ordinary seeded generation when replay matters, as it does in tests and simulations. Use a secure API when another person benefits from predicting the output.

Which generator belongs here?Could predictionhelp anattacker?YESsecure APIfor secretsNOMust the runreplayexactly?NOordinaryrandom APIYESspecified seededgenerator
Choose a generator by prediction risk first, then by replay needs.

Readers comparing the two runtime families can continue with Java vs JavaScript, especially before translating code that depends on integer behavior or browser APIs.

Test and Debug a Generator

A fixed test vector should be the first test for a portable generator. It detects arithmetic and state errors before distribution checks obscure the cause.

Use this checklist:

  • Verify known states. Start from one fixed seed and compare several consecutive raw outputs.
  • Test range boundaries. For a bound of n, every returned value must satisfy 0 <= value < n.
  • Check state storage. Confirm that each call saves the new state before the next call.
  • Look for accidental reseeding. Recreating the generator inside a loop can repeat the first value.
  • Preserve call order. Adding one random request changes every later result in that run.
  • Exercise rejection paths. Use a bound that does not divide the raw range and confirm that rejected values trigger another state update.
  • Run distribution sanity checks. Count results across buckets to catch obvious mistakes, but do not treat a visually balanced sample as proof of quality or security.
  • Test code constraints. Check length, allowed characters, invalid bounds, negative lengths, and uniqueness enforcement where the application requires it.

A generator may pass its vector while its code formatter still fails. Test the numeric layer and character layer separately. The numeric vector checks the recurrence; alphabet and length tests check the formatting logic.

Debug one layer at a timenumeric layerstate andarithmeticrawformat layerindex andalphabetknown vectorfinds state ormath bugscode testsfind alphabet orlength bugs
Test raw state transitions separately from character formatting.

Common mistakes with pseudo random code generators

  • Assuming equal seeds make unrelated language APIs produce equal sequences.
  • Forgetting that an extra call advances the state and changes every later value.
  • Using signed 32-bit JavaScript output without converting it with >>> 0.
  • Widening a negative Java int state to long without first converting it to an unsigned value.
  • Omitting the Python mask and accidentally keeping an ever-growing integer state.
  • Applying % bound without accounting for modulo bias.
  • Treating a random-looking code as guaranteed unique.
  • Using an ordinary PRNG for a secret that must resist prediction.
  • Treating a small distribution test as proof that a generator is secure.

Where the books fit

For a quick JavaScript syntax refresh before adapting the browser examples, JavaScript in Three Days covers the essentials without turning this generator into a larger project. For sustained work on testing, numeric behavior, APIs, and interview-level implementation, use JavaScript in Three Months, Python in Three Months, or Java in Three Months according to the runtime that will own the final code. Start with the fixed vector, keep the seed and call order visible, then replace the teaching generator wherever the generated code must remain secret.

Frequently asked questions

What is a pseudo random code generator?

A pseudo random code generator runs a deterministic number generator and converts its output into characters from an allowed alphabet. Its seed and changing internal state determine the sequence, so the same algorithm and calls can reproduce the same codes.

Does the same seed produce the same result in every language?

Only if every implementation uses the same algorithm, integer arithmetic, initial state, and sequence of calls. Python random, Java Random, and JavaScript Math.random use different interfaces and are not required to match one another.

Can a pseudo random generator create secure passwords or tokens?

An ordinary seeded generator should not create passwords, reset links, verification codes, or security tokens. Use Python secrets, Java SecureRandom, or Web Crypto for values that an attacker must not predict.

Are random-looking codes guaranteed to be unique?

No. Independent generation can produce the same code more than once, even with a secure generator. If duplicates are unacceptable, enforce uniqueness with stored issued values or a database constraint and retry after a collision.