Searching memory

The bread and butter of memory editing β€” find every address in the process that holds a value you can describe.

PyMemoryEditor offers three search APIs:

MethodWhat it does
search_by_valueFind every address holding a specific value (with eight comparison modes).
search_by_value_betweenFind every address whose value is inside (or outside) a range.
search_by_addressesLook up the values at a known list of addresses β€” the refine step.

For locating code or byte patterns (AOB / signatures), see the dedicated Pattern scan guide.

Search by value

from PyMemoryEditor import OpenProcess

with OpenProcess(process_name="game.exe") as process:
    for address in process.search_by_value(int, 4, 100):
        print(f"Found at 0x{address:X}")

search_by_value is a generator β€” it yields one match at a time as it scans. Wrap it with list(...) only if you actually need every match in memory.

Method signature

search_by_value(pytype, bufflength=None, value=..., scan_type=ScanTypesEnum.EXACT_VALUE, *, progress_information=False, writeable_only=False, memory_regions=None)
Parameters:
  • pytype (Type) – bool, int, float, str or bytes.

  • bufflength (int) – value size in bytes (1, 2, 4, 8). Optional β€” defaults to None: numeric types use their default width and str / bytes infer it from the encoded length of value. Since it is optional, pass value by keyword when omitting it (search_by_value(int, value=100)).

  • value – the value to look for.

  • scan_type (ScanTypesEnum) – comparison mode β€” see below.

  • progress_information (bool) – when True, yields (address, info) tuples so you can update a progress bar.

  • writeable_only (bool) – when True, only scans writable regions (faster, drops read-only static data).

  • memory_regions – an optional snapshot β€” see The refine-scan workflow.

Returns:

a generator of addresses (or (address, info) tuples).

Comparison modes

The optional scan_type controls how each value in memory is compared to your target. Every mode is a member of ScanTypesEnum:

ModeMatch when…
EXACT_VALUE (default)value == target
NOT_EXACT_VALUEvalue != target
BIGGER_THANvalue > target
SMALLER_THANvalue < target
BIGGER_THAN_OR_EXACT_VALUEvalue ≥ target
SMALLER_THAN_OR_EXACT_VALUEvalue ≤ target
VALUE_BETWEENmin ≤ value ≤ max (use search_by_value_between)
NOT_VALUE_BETWEENvalue < min or value > max
from PyMemoryEditor import OpenProcess, ScanTypesEnum

with OpenProcess(process_name="game.exe") as process:
    # Every address that holds a value bigger than 1_000_000.
    for address in process.search_by_value(
        int, 4, 1_000_000,
        scan_type=ScanTypesEnum.BIGGER_THAN,
    ):
        print(hex(address))

Showing progress

Long scans on big processes can take a while. Pass progress_information=True to get a small dict with each match:

for address, info in process.search_by_value(int, 4, target, progress_information=True):
    pct = info["progress"] * 100
    print(f"0x{address:X} | {pct:5.1f}%")

The info dict has at least a progress key (a float in [0, 1]).

Search by range

For value ranges (e.g. β€œfind every address holding 100…200”):

for address in process.search_by_value_between(int, 4, 100, 200):
    print(hex(address))

# The inverse β€” every address whose value is OUTSIDE the range:
for address in process.search_by_value_between(
    int, 4, 100, 200, not_between=True,
):
    print(hex(address))

Method signature

search_by_value_between(pytype, bufflength=None, start=..., end=..., *, not_between=False, progress_information=False, writeable_only=False, memory_regions=None)

Same parameters as search_by_value, plus:

  • start, end β€” the range boundaries (inclusive).

  • not_between β€” when True, returns values outside the range.

bufflength is optional here too (defaults to None); pass start / end by keyword when you omit it: search_by_value_between(int, start=100, end=200).

Search by addresses

When you already know which addresses to check (typically because you scanned earlier), search_by_addresses is the right tool β€” it reads each memory page only once and pulls every requested address out of it.

addresses = [0x10000, 0x10010, 0x10020, ...]

for address, value in process.search_by_addresses(int, 4, addresses):
    print(f"0x{address:X} -> {value}")

If an address falls in an unmapped page, the value is None (unless raise_error=True).

Method signature

search_by_addresses(pytype, bufflength=None, addresses=..., *, raise_error=False, memory_regions=None)
Parameters:
  • bufflength (int) – value size in bytes. Optional for numeric types (defaults to None β†’ intβ†’4, floatβ†’8, boolβ†’1). str / bytes still require an explicit size β€” there is no value to infer it from, only addresses to read. Pass addresses by keyword when omitting it.

  • addresses (Sequence[int]) – addresses to inspect.

  • raise_error (bool) – when True, raises OSError instead of yielding None for an unreadable address.

  • memory_regions – optional snapshot.

Returns:

a generator of (address, value) tuples.

The refine-scan workflow

For the classic Cheat-Engine loop β€” β€œfirst scan β†’ restrict β†’ restrict” β€” enumerate the memory regions once and reuse the snapshot across every subsequent call. On heavy targets (browsers, JVMs with 100 000+ regions) this is a massive win because the per-call region enumeration is the dominant cost otherwise.

with OpenProcess(pid=1234) as process:
    regions = process.snapshot_memory_regions()

    # First pass β€” every address holding 100.
    candidates = list(process.search_by_value(int, value=100, memory_regions=regions))

    # Refine β€” keep only those that now hold 95.
    refined = [
        addr
        for addr, value in process.search_by_addresses(int, addresses=candidates, memory_regions=regions)
        if value == 95
    ]

All of snapshot_memory_regions(), search_by_value, search_by_value_between and search_by_addresses accept the same memory_regions= keyword. Pass an empty list ([]) to explicitly scan nothing.

Keep the snapshot sorted

The snapshot is pre-sorted by base address and tagged so that helpers skip their per-call sorted(...) step on reuse. Don’t reorder the returned list manually; if you must slice or filter, pass the result of sorted(my_slice, key=...) β€” the helpers re-sort defensively when the tag is missing.

Scan acceleration (the speed extra)

By default every scan runs in pure Python, with the hottest paths already delegated to C primitives: bytes.find for exact matches, struct.iter_unpack to decode a region, and a regex byte-class prefilter for ordered string comparisons (BIGGER_THAN / SMALLER_THAN / VALUE_BETWEEN on str), which skips the long runs of non-matching bytes in C instead of stepping every offset. What stays in Python is the per-value comparison loop of the ordered numeric scans: for a multi-megabyte region it boxes and compares millions of values one at a time.

Installing the optional speed extra replaces that loop with a single vectorized NumPy comparison:

pip install "PyMemoryEditor[speed]"

There is nothing to enable β€” PyMemoryEditor detects NumPy at import time and routes the typed numeric scans through it automatically. Under the hood, each region becomes a zero-copy typed array and the comparison runs once over the whole array in C/SIMD:

arr  = np.frombuffer(region, dtype="<i4")   # bytes -> int32 array, no copy
mask = arr > target                          # one C-level comparison
offsets = np.flatnonzero(mask) * 4           # match positions -> byte offsets

Identical results, just faster

The NumPy path returns exactly the same addresses, in the same order, as the pure-Python loop β€” it is a drop-in fast path, not a behavior change. An equivalence test suite asserts this across every scan type, byte width and signedness. If NumPy is not installed, the pure-Python loop runs instead and nothing breaks.

When it helps (and when it doesn’t)

The win scales with how selective the scan is, because building the result list is work both paths share β€” the acceleration is in the comparison, not in emitting matches.

ScenarioTypical speedup
Selective scan of a large region (few matches β€” the usual first scan / refine step)10–60Γ—
Scan where most values match (e.g. > 0 on mostly-positive data)~2Γ— (result building dominates)
str ordered scans (>, <, between)no NumPy fast path β€” instead C-accelerated by the regex byte-class prefilter (independent of the speed extra)
bytes scans, or unusual widths (3/6/7 bytes)no change (no NumPy fast path; pure-Python loop)
EXACT_VALUE via search_by_valuealready bytes.find in C β€” NumPy not used

You can check whether the fast path is active:

from PyMemoryEditor.util import NUMPY_AVAILABLE
print("NumPy acceleration:", NUMPY_AVAILABLE)

Working with strings and bytes

All of the above methods work with str and bytes too:

# Find every memory address holding the literal string "PLAYER".
for address in process.search_by_value(str, 6, "PLAYER"):
    print(hex(address))

Ordering for the comparison modes differs by type:

  • str compares the UTF-8 bytes lexicographically (big-endian), so "AA" < "AB" < "B". The shorter of two values is NUL-padded to bufflength before comparing, and a reversed VALUE_BETWEEN range (start > end) simply matches nothing.

  • bytes compares using your system’s byteorder β€” something to keep in mind when using BIGGER_THAN / SMALLER_THAN on raw bytes.

See also

  • Pattern scan β€” find data by shape with regex and AOB signatures.

  • Pointers β€” once you’ve found a candidate, follow it through a pointer chain.