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:
| Method | What it does |
|---|---|
search_by_value | Find every address holding a specific value (with eight comparison modes). |
search_by_value_between | Find every address whose value is inside (or outside) a range. |
search_by_addresses | Look 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,strorbytes.bufflength (int) β value size in bytes (1, 2, 4, 8). Optional β defaults to
None: numeric types use their default width andstr/bytesinfer it from the encoded length ofvalue. Since it is optional, passvalueby 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:
| Mode | Match when⦠|
|---|---|
EXACT_VALUE (default) | value == target |
NOT_EXACT_VALUE | value != target |
BIGGER_THAN | value > target |
SMALLER_THAN | value < target |
BIGGER_THAN_OR_EXACT_VALUE | value ≥ target |
SMALLER_THAN_OR_EXACT_VALUE | value ≤ target |
VALUE_BETWEEN | min ≤ value ≤ max (use search_by_value_between) |
NOT_VALUE_BETWEEN | value < 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β whenTrue, 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/bytesstill require an explicit size β there is no value to infer it from, only addresses to read. Passaddressesby keyword when omitting it.addresses (Sequence[int]) β addresses to inspect.
raise_error (bool) β when
True, raisesOSErrorinstead of yieldingNonefor 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.
| Scenario | Typical 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_value | already 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:
strcompares the UTF-8 bytes lexicographically (big-endian), so"AA" < "AB" < "B". The shorter of two values is NUL-padded tobufflengthbefore comparing, and a reversedVALUE_BETWEENrange (start > end) simply matches nothing.bytescompares using your systemβsbyteorderβ something to keep in mind when usingBIGGER_THAN/SMALLER_THANon 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.