Molecular Fingerprints Explained
10 min read · updated August 11, 2026
A molecular fingerprint is not an embedding and nothing about it is learned. It is a deterministic inventory of the substructures present in a molecule, hashed into a fixed-width bit vector so that two molecules can be compared with integer operations.
What a fingerprint actually stores
The dominant family is the extended-connectivity fingerprint, ECFP, described by David Rogers and Mathew Hahn in their 2010 paper in the Journal of Chemical Information and Modeling. RDKit implements it under the name Morgan fingerprint, because the identifier-refinement loop is the one Harry Morgan published in 1965 for canonical numbering. ECFP and Morgan are the same algorithm with different default atom invariants.
What it stores is a set of integers. Each integer identifies one circular neighbourhood: an atom, plus every atom within some number of bonds of it, plus the bonds joining them. A molecule with 20 heavy atoms at radius 2 produces up to 60 such neighbourhoods — one per atom per iteration, including iteration 0 — and the fingerprint is the set of their identifiers. Because it is a set, a substructure appearing three times contributes one member, unless you ask for a count fingerprint instead of a bit fingerprint.
Nothing here is trained, and that is the point. The same molecule produces the same bits on any machine running the same toolkit version, which is why fingerprints remain the baseline that a learned representation has to beat rather than a legacy technique.
The Morgan iteration
Iteration 0 assigns every atom an integer hashed from its local properties. The Daylight-style invariants Rogers and Hahn use are: the number of heavy-atom neighbours, the valence minus the hydrogen count, the atomic number, the isotope, the formal charge, the number of attached hydrogens, and whether the atom is in a ring. Two atoms get the same iteration-0 identifier if and only if all seven agree.
Each subsequent iteration replaces every atom’s identifier with a hash of its own previous identifier together with the sorted list of (bond order, neighbour’s previous identifier) pairs. Sorting is what makes the result independent of atom ordering. After iteration k, an atom’s identifier summarises everything within k bonds of it, because the neighbours’ identifiers already summarised everything within k-1 bonds of them.
Identifiers from every iteration are accumulated, not just the last one. That is why a radius-2 fingerprint still contains the single-atom features: a query looking for “any aromatic nitrogen” and a query looking for a specific fused bicycle both have bits to match on.
Worked: ethanol at radius 1
Ethanol is CCO: a methyl carbon, a methylene carbon, and a hydroxyl oxygen. Call them C1, C2 and O3.
iteration 0 — invariants (degree, heavy valence, Z, charge, nH, ring)
C1 (1, 1, 6, 0, 3, no) -> id_0(C1)
C2 (2, 2, 6, 0, 2, no) -> id_0(C2)
O3 (1, 1, 8, 0, 1, no) -> id_0(O3)
three distinct identifiers, because no two atoms agree on all six
iteration 1 — hash(own id_0, sorted [(bond order, neighbour id_0)])
C1 hash(id_0(C1), [(1, id_0(C2))]) -> id_1(C1)
C2 hash(id_0(C2), [(1, id_0(C1)), (1, id_0(O3))]) -> id_1(C2)
O3 hash(id_0(O3), [(1, id_0(C2))]) -> id_1(O3)
accumulated set = { id_0(C1), id_0(C2), id_0(O3),
id_1(C1), id_1(C2), id_1(O3) } -> 6 identifiers
fold into 2048 bits: bit = identifier mod 2048, set to 1So ethanol at radius 1 sets at most six bits out of 2,048. Drug-like molecules at radius 2 typically set a few dozen to a couple of hundred: the vector is extremely sparse, and that sparsity is what makes the similarity arithmetic on fingerprint search cheap.
Folding, and the collisions it buys
The identifier set is unbounded — a hash of a hash of a hash. To get a fixed-width vector you take each identifier modulo the bit length. That is folding, and it is lossy in exactly the way you would expect: two chemically unrelated neighbourhoods can land on the same bit, and once they do, nothing downstream can tell them apart.
The practical consequence is that bit length is a knob on false similarity, not on information content. At 1,024 bits a large molecule setting 200 distinct identifiers is putting 200 draws into 1,024 buckets, and collisions are common enough to inflate pairwise similarity scores across the whole dataset. At 2,048 — RDKit and most published work default here — collisions are rarer. At 4,096 they are rarer still, at double the memory and the same popcount cost per 64-bit word.
If you need the unfolded truth, RDKit’s generators can return a sparse count fingerprint keyed on the raw identifiers, with no modulo at all. That is the right object for exact substructure bookkeeping and the wrong one for a dense matrix you intend to feed a model.
The parameters that change the answer
- Radius. The number of iterations. The ECFP naming convention counts diameter, so ECFP4 is radius 2 and ECFP6 is radius 3. Getting this backwards is the single most common error in reproducing a published fingerprint. Radius 2 is the near-universal default; radius 3 captures larger ring systems and sets more bits, which makes molecules look less similar to each other overall.
- Chirality. Off by default. Two enantiomers produce identical fingerprints unless you enable it, which is a real problem if your endpoint is stereospecific — and biological activity usually is. Turning it on adds the chiral tag to the atom invariants.
- Feature invariants. FCFP replaces the atom invariants with pharmacophoric roles — donor, acceptor, aromatic, halogen, basic, acidic. The same algorithm then produces a fingerprint that treats a chlorine and a bromine as interchangeable, which is what you want for scaffold hopping and not what you want for exact-analogue retrieval.
- Counts versus bits. A count vector keeps how many times each neighbourhood occurred. It helps for regression on extensive properties such as molar mass or lipophilicity, and it breaks the neat bitwise formulation of Tanimoto.
RDKit’s current entry point is the generator API — rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048) — and the older GetMorganFingerprintAsBitVect has been progressively steered away from. Check your version’s release notes rather than copying a decade-old snippet; the generator also exposes a bit-info map that tells you which atom and which radius set each bit, which is the only practical way to explain a model that consumes fingerprints.
Where fingerprints stop working
A fingerprint has no geometry. Two conformers of one molecule are the same fingerprint, so anything shape-dependent — how a ligand actually sits in a pocket, whether an intramolecular hydrogen bond folds the molecule — is invisible to it. It also has no global view: it records that a sulfonamide is present and that a trifluoromethyl group is present, but not that they are on opposite ends of a rigid biphenyl.
The sharpest failure is the activity cliff. Two compounds differing by one methyl group share nearly all their bits, so any fingerprint-based model interpolates smoothly between them — while the measured activity may differ by two orders of magnitude. No amount of bit width fixes that, because the information was never in the representation. It is one of the reasons learned graph representations exist, though they do not reliably fix it either.