mhctools
Python interface to MHC binding, presentation, immunogenicity, and antigen processing predictors.
Installation
pip install mhctools
For MHCflurry support, also run:
mhctools fetch mhcflurry
Predictor artifacts
mhctools exposes one acquisition command for model weights, reference files, and external tool snapshots required by its wrappers. Predictors with their own download manager keep using it; mhctools reports the manager and resolved path instead of copying the files into a second cache.
# Show packaged and optional artifacts, where they live, and who manages them.
mhctools ls
# Fetch the upstream package's default compatible release.
mhctools fetch mhcflurry
# Reproducibility runs may request an explicit artifact release.
mhctools fetch mhcflurry --version 2.2.0
# Fetch a pinned open-source snapshot (code plus the wrapper's model files).
mhctools fetch eramer
# Academic licenses must be reviewed and accepted explicitly.
mhctools fetch nettcr --accept-license
# MixTCRpred includes two upstream checkpoints; fetch another by model name.
mhctools fetch mixtcrpred --accept-license
mhctools fetch mixtcrpred --model A0201_NLVPMVATV
mhctools ls mixtcrpred --models --downloaded
# Machine-readable inventory, optionally rooted somewhere else.
mhctools ls --json
mhctools ls --data-dir /shared/models
The same operations are available in Python:
from mhctools import ERAMER, MHCflurry, fetch, list_artifacts
MHCflurry.fetch()
fetch("mhcflurry-affinity")
ERAMER.fetch()
for artifact in list_artifacts():
print(artifact.name, artifact.manager, artifact.version, artifact.path)
fetch() obtains every safely and legally downloadable artifact needed by the
named wrapper. It does not install Python packages, execute upstream setup
scripts, or duplicate caches owned by another package.
mhctools-managed snapshots default to the platform's user data directory
(~/Library/Application Support/mhctools on macOS,
~/.local/share/mhctools on Linux). Set MHCTOOLS_DATA_DIR, pass
--data-dir, or use the Python data_dir= argument to put them on shared or
scratch storage. Every snapshot lives under
artifacts/<tool>/<git-commit>/ and includes .mhctools-artifact.json with
its source repository, exact commit, sparse paths, and license provenance.
The MANAGER column distinguishes four ownership models:
mhctools package/<package> package: weights shipped in an installed Python package;mhcflurry: MHCflurry's own native download cache;mhctools: a pinned snapshot fetched into the directory above;user/manual: an existing checkout or licensed executable owned by the user. Manual artifacts are listed butfetchwill not redistribute them.
Small published models such as Calis remain fully embedded in the mhctools
package and appear as mhctools package; they never require a separate fetch.
The DTU NetMHC-family downloads are identity-bound licenses: DTU requires a
name, position, academic email, affiliation, acceptance, and then sends a
private download link. Therefore --accept-license cannot substitute for the
official DTU request form, and these installations remain manual inventory.
Quick start
from mhctools import NetMHCpan41
predictor = NetMHCpan41(alleles=["HLA-A*02:01", "HLA-B*07:02"])
# predict() returns a list of PeptideResult — one per peptide
results = predictor.predict(["SIINFEKL", "GILGFVFTL"])
for r in results:
if r.affinity:
print(f"{r.peptide} -> {r.affinity.allele} IC50={r.affinity.value:.1f}nM")
Data model
predict() returns a list of PeptideResult — one per peptide. Each
result carries the peptide string and provides accessors for each
prediction kind (affinity, presentation, stability, etc.). Accessors
return None when a predictor doesn't produce that kind.
results = predictor.predict(["SIINFEKL", "GILGFVFTL"])
r = results[0]
r.peptide # "SIINFEKL"
r.affinity.value # IC50 in nM
r.affinity.percentile_rank # 0-100, lower = better
r.affinity.allele # best allele for this kind
r.presentation # None if predictor doesn't produce it
Under the hood, each PeptideResult wraps a tuple of Prediction objects —
frozen dataclasses, one per allele-kind combination. Everything converts
to DataFrames with consistent column names.
Python API
Predicting peptides
from mhctools import NetMHCpan41
predictor = NetMHCpan41(alleles=["HLA-A*02:01", "HLA-B*07:02"])
results = predictor.predict(["SIINFEKL", "GILGFVFTL"])
r = results[0]
r.peptide # "SIINFEKL"
r.offset # position in source protein (if scanned)
r.kinds # {"pMHC_affinity", "pMHC_presentation"}
r.alleles # {"HLA-A*02:01", "HLA-B*07:02"}
# best prediction by kind — None when the kind is absent
r.affinity # Prediction or None
r.presentation # Prediction or None
r.stability # None (predictor doesn't produce it)
if r.affinity:
r.affinity.value # IC50 in nM
r.affinity.percentile_rank # 0-100, lower = better
r.affinity.score # predictor-specific scale, higher = better
r.affinity.allele # best allele for this kind
# by rank instead of score
r.best_affinity_by_rank # Prediction with lowest percentile rank, or None
# all predictions
r.preds # tuple of all Prediction objects
r.filter(kind="pMHC_affinity")
r.filter(allele="HLA-A*02:01")
NetMHCpan 4.1 automatically emits both pMHC_affinity and pMHC_presentation
predictions per peptide-allele pair.
Scanning proteins
predict_proteins() takes a dictionary of protein sequences and returns
{sequence_name: list[PeptideResult]}:
proteins = predictor.predict_proteins(
{"TP53": "MEEPQSDPSVEPPLSQETFS...", "KRAS": "MTEYKLVVVGAGGVGKS..."},
peptide_lengths=[9, 10],
)
for r in proteins["TP53"]:
if r.affinity and r.affinity.value < 500:
print(f" offset={r.offset} {r.peptide} IC50={r.affinity.value:.0f}")
DataFrames
Every level has a _dataframe variant that flattens to a pandas DataFrame
with consistent columns:
df = predictor.predict_dataframe(["SIINFEKL"], sample_name="pat001")
df = predictor.predict_proteins_dataframe({"TP53": "MEEPQ..."}, sample_name="pat001")
Columns: sample_name, peptide, n_flank, c_flank,
source_sequence_name, offset, predictor_name, predictor_version,
allele, kind, score, value, percentile_rank.
Multi-sample predictions
MultiSample runs a predictor across multiple samples, each with its own
HLA genotype:
from mhctools import MultiSample, NetMHCpan41
ms = MultiSample(
samples={
"pat001": ["HLA-A*02:01", "HLA-B*07:02"],
"pat002": ["HLA-A*01:01", "HLA-B*08:01"],
},
predictor_class=NetMHCpan41,
)
# {sample_name: list[PeptideResult]}
results = ms.predict(["SIINFEKL", "GILGFVFTL"])
# {sample_name: {seq_name: list[PeptideResult]}}
protein_results = ms.predict_proteins({"TP53": "MEEPQ..."})
# flat DataFrames with sample_name column
df = ms.predict_dataframe(["SIINFEKL"])
df = ms.predict_proteins_dataframe({"TP53": "MEEPQ..."})
Measurement kinds and MHC context
Each Prediction has a kind string describing what it measures:
The canonical prediction kind strings are defined in mhctools.pred.Kind.
| Kind | Meaning | value unit |
|---|---|---|
pMHC_affinity |
Peptide-MHC binding affinity | nM (IC50) |
pMHC_presentation |
Likelihood of surface presentation (EL/processing) | — |
pMHC_stability |
Peptide-MHC complex stability | hours (Thalf) |
pMHC_TCR_binding |
TCR recognition of a peptide-MHC (pMHC:TCR binding) | — |
immunogenicity |
T-cell immunogenicity | — |
antigen_processing |
Combined processing score | — |
proteasome_cleavage |
Proteasomal (MHC-I, cytosolic) C-terminal cleavage score | — |
endolysosomal_cleavage |
Endolysosomal (MHC-II, cathepsin) C-terminal cleavage score | — |
tap_transport |
TAP transport / binding score | nM |
erap_trimming |
ERAP1 N-terminal trimming score | — |
peptide_half_life |
Parent-peptide half-life; matrix and systemic scope live in context | hours |
systemic_clearance |
Systemic or apparent clearance | context-defined |
distribution_volume |
Systemic or apparent distribution volume | context-defined |
systemic_exposure |
Systemic exposure, such as AUC | context-defined |
cpp_classification |
CPP class label and confidence | — |
cellular_uptake |
Quantitative uptake in a named cellular context | context-defined |
tissue_concentration |
Concentration in a named tissue/compartment and timepoint | context-defined |
Units
Kind and unit are independent. Every prediction has a kind, because every
prediction measures something; only some kinds have a unit. A model that emits
a bare 0–1 confidence is still a prediction of a kind — it just fills score and
leaves value empty. Fill in both wherever the predictor supports it.
score— always present and uses higher-is-better ordering, but its scale and units are predictor-specific. It may be a probability, an uncalibrated model output, a transformed estimate, or a copy ofvalue; consult the predictor's documentation before comparing or thresholding it. For example, PeptiVerse repeats its predicted hours in bothscoreandvalue, while PlifePred2 keeps its unresolved native output inscoreand leavesvalueempty unless its inferred conversion is explicitly enabled.value— present only for the kinds marked above, carrying a physical quantity on a linear scale in that unit: never a log, never a rescaling, never whatever the upstream tool happened to print. A kind having a unit does not oblige every predictor to fill it — one whose transform to that unit is unresolved should leavevalueempty rather than guess (seePlifePred2).percentile_rank— present when the predictor scores against a background distribution. Always lower-is-better.
For affinity predictions, score is commonly the monotone 1-log50k
rescaling and value is the estimated IC50 in nM. The rescaling is useful for
ordering predictions; it is not a calibrated probability or confidence, and
it is not inherently bounded to 0–1.
from mhctools import Kind
from mhctools.pred import value_unit
value_unit(Kind.pMHC_affinity) # 'nM'
value_unit(Kind.peptide_half_life) # 'hours'
value_unit(Kind.immunogenicity) # None
Converting is the wrapper's job, and it long predates the registry: affinity
predictors commonly work in 1-log50k space internally and every affinity
wrapper here inverts it to nM, so a NetMHCpan IC50 and an MHCflurry IC50 are
directly comparable. PeptiVerse's upstream sequence model applies its
log1p(hours) inverse and the wrapper reports hours. PlifePred2's target
transform and assay provenance remain unresolved, so the wrapper reports only
the native score by default; assume_log10_seconds=True opts into the inferred
conversion to hours.
A predictor's native output isn't lost, it just doesn't belong in a
units-bearing field. Wrappers keep it on their last_qc frame:
predictor.predict(["SIINFEKLGGALQAKKY"])
predictor.last_qc["log10_seconds"] # PlifePred2's raw model output
Note that sharing a unit is not sharing a measurement: pMHC_stability is the
lifetime of a peptide-MHC complex, while peptide_half_life is the lifetime of
the parent peptide. Serum, plasma, whole blood, cellular, and systemic settings
share the latter kind and remain distinct through MeasurementContext.
Higher-is-better is only a numerical selection convention within a documented endpoint. It does not mean that a larger score is universally better for a vaccine, and it does not make scores from different predictors or endpoints interchangeable.
Every prediction carries a small immutable MeasurementContext; ordinary
predictors get a shared default automatically, while assay-specific wrappers
fill only the fields they know. Equal contexts are interned and reused. PK,
uptake, tissue, and peptide-half-life results add explicit units, matrices,
compartments, scope, or time identity as needed. See
Peptide PK, uptake, and tissue-exposure results.
Predictors also expose kind_support() so downstream code can tell what MHC
context is meaningful for each emitted kind:
support = predictor.kind_support()
support["pMHC_affinity"]
# {"mhc_dependence": "single_allele", "mhc_class": "I"}
mhc_dependence is one of:
| Value | Meaning |
|---|---|
none |
The prediction is MHC-independent; Prediction.allele is empty. |
single_allele |
The prediction is for one peptide/MHC allele pair; Prediction.allele is part of the key. |
haplotype |
The prediction uses the requested MHC repertoire jointly; Prediction.allele may carry best-allele attribution but is not the prediction key. |
mhc_class is one of none, I, II, or both.
The allowed metadata values are defined in mhctools.pred as
MHC_DEPENDENCE_VALUES and MHC_CLASS_VALUES.
Examples:
| Predictor | Kind | mhc_dependence |
mhc_class |
|---|---|---|---|
NetMHCpan41 |
pMHC_affinity |
single_allele |
I |
NetMHCpan41 |
pMHC_presentation |
single_allele |
I |
NetMHCIIpan4_EL |
pMHC_presentation |
single_allele |
II |
CapHLA |
pMHC_affinity |
single_allele |
both |
CapHLA |
pMHC_presentation |
single_allele |
both |
MixMHC2pred |
pMHC_presentation |
single_allele |
II |
NetMHCstabpan |
pMHC_stability |
single_allele |
I |
MHCflurry |
pMHC_affinity |
single_allele |
I |
MHCflurry haplotype mode |
pMHC_presentation |
haplotype |
I |
MHCflurry per-allele panel mode |
pMHC_presentation |
single_allele |
I |
MHCflurry |
antigen_processing |
none |
none |
Pepsickle |
proteasome_cleavage |
none |
none |
NetCleave_I |
proteasome_cleavage |
none |
I |
NetCleave_II |
endolysosomal_cleavage |
none |
II |
DeepTAP |
tap_transport |
none |
none |
ERAMER |
erap_trimming |
none |
I |
NetTCR |
pMHC_TCR_binding |
none |
I |
Tulip |
pMHC_TCR_binding |
single_allele |
I |
MixTCRpred |
pMHC_TCR_binding |
single_allele |
model-specific |
BigMHC_IM |
immunogenicity |
single_allele |
I |
PRIME |
immunogenicity |
single_allele |
I |
DeepImmuno |
immunogenicity |
single_allele |
I |
TLimmuno2 |
immunogenicity |
single_allele |
II |
Calis |
immunogenicity |
none |
I |
PeptiVerse |
peptide_half_life |
none |
none |
PlifePred2 |
peptide_half_life |
none |
none |
TCR predictors (NetTCR, Tulip, MixTCRpred)
NetTCR and Tulip predict pMHC:TCR binding — whether a paired αβ T-cell
receptor (an mhctools.TCR, described by its CDR loops) recognises a peptide.
Both take (peptide, TCR) inputs; Tulip additionally takes the presenting
MHC allele.
MixTCRpred has a different, deliberately explicit shape: each checkpoint is
trained for one fixed peptide/MHC target. Its catalog currently contains 147
models (43 marked high-confidence by upstream), spanning human/mouse class I
and II targets. The input TCR stores paired CDR3s and optional trav, traj,
trbv, and trbj assignments. Released models use CDR3alpha/beta plus CDR1/2
derived from the V genes; J assignments are accepted and QC-reported but are
not network inputs.
pip install "mhctools[mixtcrpred]"
mhctools fetch mixtcrpred --accept-license
mhctools ls mixtcrpred --models --high-confidence
mhctools fetch mixtcrpred --model A0201_GILGFVFTL
mhctools mixtcrpred --model A0201_GILGFVFTL \
--input paired-tcrs.csv --out scored-tcrs.csv
from mhctools import MixTCRpred, TCR
models = MixTCRpred.catalog()
target = MixTCRpred.resolve_model("GILGFVFTL", "HLA-A*02:01")
predictor = MixTCRpred(target.name)
tcr = TCR(
cdr3a="CAGASGNTGKLIF", cdr3b="CASSIRASYEQYF",
trav="TRAV27", traj="TRAJ42", trbv="TRBV19", trbj="TRBJ2-6",
)
prediction = predictor.predict_tcrs([tcr])[0]
prediction.score
prediction.percentile_rank
annotate_dataframe() and the CSV command retain the original table and add
the raw score, percentile rank, fixed target metadata, corrected V/J names,
V-derived CDR1/2 loops, and upstream-equivalent QC warning. MixTCRpred code is
academic/non-commercial and fetched directly from its original repository
only after explicit acceptance. Optional checkpoints come from the authors'
immutable CC-BY-4.0 Zenodo record and are checksum-verified before use.
As with any PyTorch checkpoint, explicit overrides and user-managed model files
must come from a trusted source because loading can execute serialized code.
from mhctools import Tulip, TCR
tcr = TCR(cdr3a="CAGASGNTGKLIF", cdr3b="CASSIRASYEQYF", name="clone1")
Tulip.fetch() # pinned code, tokenizers, and weights
predictor = Tulip() # also needs a TULIP-capable Python
results = predictor.predict(["GILGFVFTL"], [tcr], mhc="HLA-A*02:01")
results[0].preds[0].score # higher = more likely binding
TULIP-TCR is GPLv3 and pinned to
transformers==4.32.1; mhctools is Apache-2.0 and depends on neither torch nor
transformers. The Tulip wrapper therefore vendors none of TULIP — it runs an
upstream checkout out-of-process, in an isolated interpreter, via TULIP's own
predict.py. mhctools fetch tulip obtains the tested code, tokenizers, and
weights; scripts/setup_tulip_env.sh can build the separate runtime. You may
instead provide your own checkout and interpreter:
TULIP_HOME— a clone of TULIP-TCR (providespredict.py,src/, tokenizers, and the releasedmodel_weights/);TULIP_PYTHON— an isolated Python 3.11 interpreter withtorchandtransformers==4.32.1(3.11 sotokenizersinstalls from a prebuilt wheel and needs no Rust toolchain).
For MHCflurry presentation, presentation_allele_mode="haplotype" treats the
requested alleles as one sample genotype and emits one pMHC_presentation
record per peptide. The allele field carries MHCflurry's best_allele
attribution when available. presentation_allele_mode="per_allele" treats each
allele as a separate one-allele synthetic sample and emits one presentation
record per peptide/allele pair. The default "auto" mode uses haplotype mode
for up to six alleles and per-allele mode for larger allele panels.
MHCflurry predictions carry the Python package and official model-release
identity in predictor_version (for example, 2.2.1+release-2.2.0). The version
is captured when weights are loaded and retained with the cached model object.
mhcflurry_composite_version() exposes the same rule publicly; it checks the
selected directory, including environment overrides, against the official
bundle path. This is release provenance, not a checksum of the weights.
For custom paths or injected predictors, pass predictor_version="my-model-id"
to MHCflurry or MHCflurry_Affinity if the predictions need a cacheable
identity. Otherwise they remain unversioned, never mislabeled as the active
default release. The modern prediction and DataFrame APIs retain the version;
legacy BindingPrediction objects still have their original unversioned schema.
The Prediction object
Every prediction is a frozen, self-contained Prediction dataclass:
from mhctools import Prediction
pred = Prediction(
kind="pMHC_affinity",
score=0.85, # predictor-specific scale, higher = better
peptide="SIINFEKL",
allele="HLA-A*02:01",
value=120.5, # IC50 in nM
percentile_rank=0.8,
source_sequence_name="TP53",
offset=42,
predictor_name="netMHCpan",
predictor_version="4.1",
)
score is always higher-is-better. value is in canonical physical units
(nM for affinity, hours for stability). percentile_rank is always optional,
0-100, lower = stronger.
Supported predictors
MHC binding & presentation
| Predictor | Kinds produced | Requires |
|---|---|---|
NetMHCpan / NetMHCpan41 / NetMHCpan42 |
affinity + presentation | NetMHCpan |
NetMHCpan4 |
affinity or presentation | NetMHCpan 4.0 |
NetMHCpan3 / NetMHCpan28 |
affinity | older NetMHCpan |
NetMHC / NetMHC3 / NetMHC4 |
affinity | NetMHC |
NetMHCIIpan / NetMHCIIpan43 |
affinity or presentation | NetMHCIIpan |
NetMHCcons |
affinity | NetMHCcons |
NetMHCstabpan |
stability | NetMHCstabpan |
MHCflurry |
affinity + presentation + processing | pip install mhcflurry + mhctools fetch mhcflurry |
MHCflurry_Affinity |
affinity | pip install mhcflurry + mhctools fetch mhcflurry-affinity |
BigMHC |
presentation or immunogenicity | mhctools fetch bigmhc --accept-license + PyTorch, or set BIGMHC_DIR |
CapHLA / CapHLA_EL / CapHLA_BA |
presentation + affinity (class I and II) | pip install "mhctools[caphla]" + mhctools fetch caphla |
MixMHCpred |
presentation (class I) | MixMHCpred |
MixMHC2pred |
presentation (class II) | MixMHC2pred release (has PWMdef/) |
IedbNetMHCpan / IedbSMM / IedbNetMHCIIpan |
affinity | IEDB web API |
RandomBindingPredictor |
affinity | (built-in) |
CapHLA is a 2025 MIT-licensed PyTorch model family (Chang & Wu,
Briefings in Bioinformatics) covering
human and mouse MHC class I and II, with peptides from 7–25 residues. The default wrapper emits
both outputs for every peptide/allele pair: EL presentation_score as
pMHC_presentation, and the BA normalized score as pMHC_affinity. For BA,
mhctools also inverts CapHLA's training transform to provide predicted IC50 nM
in value. Upstream provides neither percentile ranks nor binder thresholds,
so the wrapper does not invent them. CapHLA_EL and CapHLA_BA load only the
five-fold ensemble they need.
pip install "mhctools[caphla]"
mhctools fetch caphla
from mhctools import CapHLA
predictor = CapHLA(alleles=[
"HLA-A*02:01",
"HLA-DPA1*01:03-DPB1*04:01",
])
results = predictor.predict(["GILGFVFTL", "GELIGTLNAAKVPAD"])
results[0].presentation.score
results[0].affinity.score
results[0].affinity.value # predicted IC50, nM
# Explicit pairs preserve order and duplicates without a cross product.
paired = predictor.predict_pairs([
("GILGFVFTL", "HLA-A*02:01"),
("GELIGTLNAAKVPAD", "HLA-DPA1*01:03-DPB1*04:01"),
])
The wrapper loads the pinned upstream model definitions and weights unchanged, batches inference deterministically in-process, and preserves canonical mhcgnomes allele identity in its outputs. CapHLA performance numbers are author-reported; treat it as a complementary research predictor rather than a default or independent validation.
MixMHCpred 3.0 predicts class-I presentation for peptides of length
8-14. Version 3.0 adds pan-allele inference, MHC-I sequence alignment and
sequence-driven prediction, and optional binding-motif/peptide-length plots.
mhctools exposes all per-allele scores and percentile ranks through the
canonical prediction API. predict_detailed also retains MixMHCpred's raw
Score_bestAllele, BestAllele, and %Rank_bestAllele columns plus each
allele's closest training allele, sequence distance, and pan-allele status.
MixMHCpred 3.0 is licensed for academic, non-commercial research and prohibits redistribution without written permission, so its approximately 200 MB of code, models, and reference data are not included in mhctools. Review the upstream license and installation guide before downloading the official tagged release:
git clone --branch v3.0 --depth 1 \
https://github.com/GfellerLab/MixMHCpred.git
chmod +x MixMHCpred/MixMHCpred
export MIXMHCPRED_PATH="$PWD/MixMHCpred"
pip install "mhctools[mixmhcpred]"
The mixmhcpred extra installs the upstream Python dependencies. Sequence
alignment additionally needs the mafft executable. The upstream
install_packages script is another way to install both sets of dependencies.
from mhctools import MixMHCpred
predictor = MixMHCpred(
alleles=["HLA-A*02:01", "HLA-A*01:02"], # A*01:02 uses v3 pan inference
)
# Canonical mhctools output: one pMHC_presentation Prediction per allele.
results = predictor.predict(["SIINFEKL"])
results[0].presentation.score
# Complete native output and v3 quality/provenance metadata.
detailed = predictor.predict_detailed(["SIINFEKL"])
detailed.table[["Score_bestAllele", "BestAllele", "%Rank_bestAllele"]]
detailed.allele_info[1].closest_training_allele
detailed.allele_info[1].distance
detailed.allele_info[1].pan_allele
# Retain Binding_predictions.txt, PWM/PLD files and images, and the HTML view.
motifs = predictor.predict_detailed(
["SIINFEKL"], output_dir="mixmhcpred-output", output_motifs=True)
motifs.artifacts.files
# Align novel MHC-I sequences, then optionally predict and render their motifs.
sequence_result = predictor.predict_allele_sequences(
"unaligned-mhc-i.fasta",
peptides=["SIINFEKL"],
output_dir="mixmhcpred-sequence-output",
output_motifs=True,
)
sequence_result.aligned_sequences
sequence_result.table
sequence_result.allele_info[0].closest_database_allele
sequence_result.artifacts.files
Both artifact APIs require a new output path: the wrapper refuses an existing
path because MixMHCpred itself deletes and recreates its output directory.
exclude_peptides_with_cysteine=True is implemented by mhctools before the
external call, including under v3.0 where the legacy -c option was removed.
MixMHC2pred is a pan-allele class-II presentation predictor and a strong
complement to NetMHCIIpan (independently co-best in the Frontiers in
Immunology 2024 class-II benchmark). It emits one pMHC_presentation
prediction per (peptide, allele): score is the raw MixMHC2pred score (higher
= better), percentile_rank is its %Rank (lower = better). It's academic /
non-commercial licensed, so mhctools shells out to a user-provided install
(download a release, not a bare clone — the release ships the PWMdef/
allele definitions). Alleles may be given in the usual spellings
(HLA-DRB1*15:01) or MixMHC2pred's own (DRB1_15_01,
DQA1_01_02__DQB1_06_02).
from mhctools import MixMHC2pred
predictor = MixMHC2pred(
alleles=["HLA-DRB1*15:01", "HLA-DQA1*01:02-DQB1*06:02"],
program_name="/path/to/MixMHC2pred_unix") # MixMHC2pred on macOS
results = predictor.predict(["GELIGTLNAAKVPAD"]) # class-II length peptides
results[0].presentation.score
Antigen processing
| Predictor | Kinds produced | Requires |
|---|---|---|
Pepsickle |
proteasome cleavage | pip install pepsickle (paper) |
NetChop |
proteasome cleavage | NetChop |
NetCleave_I / NetCleave_II |
proteasomal (I) / endolysosomal (II) C-terminal cleavage | NetCleave clone (set NETCLEAVE_DIR) |
Pepsickle and NetChop use configurable scoring to aggregate per-position
cleavage probabilities into peptide-level scores (see ProcessingPredictor
and ProteasomePredictor).
NetCleave is different: it emits a single C-terminal cleavage score per
peptide and covers both the MHC-I proteasomal (NetCleave_I →
proteasome_cleavage) and MHC-II endolysosomal (NetCleave_II →
endolysosomal_cleavage) pathways — MHC-II processing is otherwise a gap in
the predictor set. It needs the residues downstream of the peptide to build
the cleavage site, so pass c_flanks (or scan proteins). Its weights ship in
the git repo; the R dependency in NetCleave's README is only for its training
pipeline, not prediction.
from mhctools import NetCleave_II
predictor = NetCleave_II() # resolves NETCLEAVE_DIR / ~/NetCleave
# score peptides with their C-terminal flanking residues (>= 3)
results = predictor.predict(["SIINFEKL"], c_flanks=["DGH"])
results[0].endolysosomal_cleavage.score
# or scan a protein so each peptide is scored in real context
by_protein = predictor.predict_proteins({"TP53": "MEEPQ..."}, peptide_lengths=[15])
⚠️ NetCleave's own paper reports class-II C-terminal cleavage is a much weaker signal than class I (AUC ~0.66 vs ~0.91). Treat
endolysosomal_cleavagescores accordingly.
TAP transport
| Predictor | Kinds produced | Requires |
|---|---|---|
DeepTAP |
TAP transport (tap_transport) |
mhctools fetch deeptap + a DeepTAP-capable Python |
TAP (transporter associated with antigen processing) is the step that shuttles
cytosolic peptides into the ER for MHC-I loading — a distinct part of the
processing pathway from proteasomal cleavage, and otherwise a gap in the
predictor set. DeepTAP is a BiGRU that scores each peptide once
(allele-independent, like the cleavage predictors), emitting one
tap_transport prediction per peptide with an empty allele. score is in
0-1 (higher = stronger TAP binding); in task_type="reg" mode the predicted
affinity in nM is also surfaced as value (lower = stronger).
DeepTAP ships its weights in-repo and is Apache-2.0, but pins an old
pytorch-lightning, so mhctools shells out to DeepTAP's own CLI in a
separate interpreter (the checkpoints load fine under modern Lightning too).
Run mhctools fetch deeptap; if the current interpreter lacks torch, set
DEEPTAP_PYTHON to one that has it. DEEPTAP_HOME can still select a manual
checkout.
from mhctools import DeepTAP
DeepTAP.fetch()
predictor = DeepTAP(task_type="cla") # resolves DEEPTAP_HOME / ~/DeepTAP
results = predictor.predict(["SIINFEKL", "AEASAAAAY"])
results[1].tap_transport.score # 0-1, higher = stronger TAP binding
⚠️ DeepTAP's evaluation is self-reported, and no independent TAP benchmark exists for any tool (true of the whole TAP field). Treat the score as a useful pathway signal for prioritization, not a validated oracle.
ERAP1 trimming
| Predictor | Kinds produced | Requires |
|---|---|---|
ERAMER |
ERAP1 trimming (erap_trimming) |
mhctools fetch eramer + openpyxl |
ERAP1 trims the N-termini of 9–16mer precursor peptides in the ER down to the
8–10mers MHC-I presents — the step between TAP transport and MHC loading, and
otherwise the last empty stage in the pathway. ERAMER scores a precursor by
averaging a per-length position-weight-matrix specificity over each residue
trimmed off as it is cut toward a target epitope length (allele-independent, one
erap_trimming prediction per peptide; score roughly −1…1, higher = more
likely trimmed).
ERAMER is GPLv3 and its PWM ships in a GPL-licensed PWM.xlsx, so mhctools
vendors neither: this is a clean-room Python-3 reimplementation of the
(Python-2.7) tool's trimming-cascade average that loads the PWM from a
upstream ERAMER checkout at runtime. Run mhctools fetch eramer, or point at a
manual clone with ERAMER_HOME.
from mhctools import ERAMER
ERAMER.fetch()
predictor = ERAMER(epitope_length=8) # resolves ERAMER_HOME / ~/ERAMER
results = predictor.predict(["GGGGGVVVVVVAAAEE"]) # a 9-16mer precursor
results[0].erap_trimming.score
⚠️ ERAMER's evaluation is self-reported and ERAP1 trimming is an intrinsically noisy signal; treat the score as a pathway prior, not a validated oracle.
Peptide half-life
| Predictor | Kinds produced | Requires |
|---|---|---|
PeptiVerse |
Parent-peptide half-life in human serum | pinned PeptiVerse + ESM2 snapshots (PEPTIVERSE_HOME, PEPTIVERSE_ESM_HOME) + a torch/transformers Python |
PlifePred2 |
Parent-peptide half-life, matrix unknown ⚠️ | plifepred2==1.0 (PLIFEPRED2_HOME) + pinned Pfeature (PFEATURE_HOME) |
peptide_half_life records how long the parent peptide persists, in hours. Its
context distinguishes a defined solution, serum/plasma/whole blood, cellular
compartments, and systemic in-vivo PK without multiplying kind strings.
It is deliberately a separate kind from pMHC_stability, which is the
dissociation half-life of an assembled peptide-MHC complex — a different
molecule in a different assay — and from the cleavage kinds, which are
site-resolved and intracellular. MeasurementContext preserves the matrix,
compartment, analyte, and systemic scope when they are known.
PeptiVerse wraps one endpoint of the upstream multi-property platform. Its
dependencies (torch, transformers==4.46.0, xgboost, lightning, and ESM2) stay
out of the mhctools environment: inference runs offline in a subprocess under
PEPTIVERSE_PYTHON. Provision the exact snapshots before prediction:
git clone https://huggingface.co/ChatterjeeLab/PeptiVerse
git -C PeptiVerse checkout 8cf0b21dae356278ae96b414a088e4360357d16c
huggingface-cli download facebook/esm2_t33_650M_UR50D \
--revision 08e4846e537177426273712802403f7ba8261b6c \
--include config.json tokenizer_config.json special_tokens_map.json vocab.txt model.safetensors \
--local-dir /models/esm2_t33_650M_UR50D
export PEPTIVERSE_HOME="$PWD/PeptiVerse"
export PEPTIVERSE_ESM_HOME=/models/esm2_t33_650M_UR50D
from mhctools import PeptideContext, PeptideInput, PeptiVerse
predictor = PeptiVerse(device="cpu") # resolves PEPTIVERSE_HOME / ~/PeptiVerse
exact_input = PeptideInput(
"SIINFEKL",
occurrence_id="sample-1:occurrence-2",
context=PeptideContext(matrix="serum", assay_species="Homo sapiens"),
)
results = predictor.predict([exact_input, "KLGGALQAK"])
results[0].peptide_half_life.value # hours, higher = longer-lived
results[0].serum_half_life.peptide_input # exact chemistry + context
results[0].serum_half_life.cache_key # input + assets + settings
predictor.artifact_inventory.to_dict() # exact files, hashes, capability
Sequence input only. Upstream's SMILES models return a number that is not on
the hours scale (the expm1 inverse transform is applied only to the sequence
model). Although mhctools records exact chemical form, this adapter does not
consume it, so terminal modifications, attachments, and non-standard residues
are rejected rather than scored as their unmodified sequence. Pass
on_unsupported="record" to retain unsupported entries in a mixed batch.
⚠️ The sequence half-life model was fit on 130 examples and evaluated by cross-validation only, from a preprint, with no external test set and no evaluation on long vaccine peptides. Upstream declares Apache-2.0 on its model card and MIT in its README. mhctools verifies the exact inference source, model, calibration, ESM2 weights, configuration, and tokenizer files before launch. The PeptiVerse checkpoint and calibration still use unsafe pickle serialization; matching a checksum establishes identity, not safety. Use only snapshots you trust.
⚠️ This endpoint's semantics are not established. PlifePred2 ships no publication, no training data and no target definition, so its units, transform, species and assay matrix are all inferred from the artifacts. By default the wrapper reports only the model's native output and claims no duration at all.
from mhctools import PlifePred2
predictor = PlifePred2() # PLIFEPRED2_HOME + PFEATURE_HOME
results = predictor.predict(["SIINFEKLGGALQAKKY"])
results[0].peptide_half_life.score # native output, higher = longer-lived
results[0].peptide_half_life.value # None by default
predictor.artifact_inventory.to_dict() # exact files, hashes, capability
predictor.last_qc["log10_seconds"] # the same value, named
# Opt in to a duration, accepting the inference below:
opted_in = PlifePred2(assume_log10_seconds=True)
opted_in.predict(["SIINFEKLGGALQAKKY"])[0].peptide_half_life.value # hours
What is known. Both shipped models are RandomForestRegressor, verified by
loading them. So the output is not a class probability — upstream's docs
("Halflife … Predicted probability") and its CLI's predict_proba branch are
both wrong, and the branch is dead code. Being monotone in half-life, the
output ranks correctly whatever the transform turns out to be.
What is inferred. log10(half-life in seconds) is the strongest reading:
inverting the forests' extreme leaf values under it gives round durations —
exactly 7.000 days for the natural model, 95.0 days for the modified one — to
about seven significant figures, where log2 and ln both invert the whole
training range to a few seconds up to a couple of minutes. The minimum also
lands on 20.2 s, matching the 20-second floor in the lineage paper. That last
point is corroboration rather than proof: the same forests hold targets past
that paper's 24-hour ceiling, so PlifePred2 was trained on a different dataset
and the old filter cannot establish the new target. Note also that the lineage
paper states log2, not log10.
What is not established. The species and assay matrix. The result therefore
uses generic peptide_half_life with matrix=None; do not report it as a
measured whole-blood property or treat it as interchangeable with PeptiVerse's
human-serum endpoint.
Natural peptides only, 12–100 residues. Upstream's CLI silently drops
out-of-range and modified sequences into an eliminated_sequences.csv and
returns a shorter result set; mhctools rejects them instead so a caller never
gets a quietly truncated answer.
The Linux-only pfeature_comp binary that plifepred2 bundles is not used
— it is a PyInstaller freeze of Pfeature's pfeature_comp.py, and that plain
Python source computes the same descriptor on any platform. Both upstreams are
GPLv3, so neither is vendored and neither is imported into the mhctools
interpreter.
⚠️ PlifePred2 cites no publication of its own, so its training set is unverified beyond what the artifacts reveal. In the lineage paper the composition-based natural model was the weaker of the pair (r = 0.643 against 0.743), and sequences up to 90% similar were deliberately kept in the data, so reported accuracy is optimistic for novel peptides.
Immunogenicity
| Predictor | Kinds produced | Requires |
|---|---|---|
Calis |
immunogenicity | nothing — self-contained |
BigMHC_IM |
immunogenicity | mhctools fetch bigmhc --accept-license + PyTorch, or set BIGMHC_DIR |
PRIME |
immunogenicity | PRIME clone + MixMHCpred |
DeepImmuno |
immunogenicity | mhctools fetch deepimmuno + a TensorFlow/Keras-2-capable Python |
TLimmuno2 |
immunogenicity (class II) | TLimmuno2 clone (set TLIMMUNO2_HOME) |
Calis is the classic sequence-only IEDB class-I immunogenicity model (Calis et
al. 2013): a fixed per-amino-acid log-enrichment scale weighted by per-position
importance, with the anchor positions (P1/P2/C-terminus) masked out. It needs
no external install and no downloaded weights — the ~30 published parameters
(from the open-access CC-BY paper) are built in — so it is a fast,
dependency-free, allele-independent baseline. It emits one immunogenicity
prediction per peptide (empty allele); score > 0 leans immunogenic.
from mhctools import Calis
predictor = Calis()
results = predictor.predict(["GILGFVFTL", "NLVPMVATV"])
results[0].immunogenicity.score # 0.30484 (higher = more immunogenic)
PRIME predicts CD8+ T-cell immunogenicity of class-I peptides by combining
MHC-I binding (via MixMHCpred, which it calls internally) with a TCR-recognition
propensity model. It emits one immunogenicity prediction per (peptide, allele):
score is the PRIME score (higher = more immunogenic) and percentile_rank is
the PRIME %Rank (lower = better). PRIME is academic / non-commercial licensed, so
mhctools shells out to a user-provided install rather than vendoring it.
from mhctools import PRIME
predictor = PRIME(
alleles=["HLA-A*02:01", "HLA-B*07:02"],
program_name="PRIME", # or an absolute path
mixmhcpred_path="/path/to/MixMHCpred") # optional if MixMHCpred is on PATH
results = predictor.predict(["GILGFVFTL", "NLVPMVATV"])
results[0].immunogenicity.score
DeepImmuno predicts class-I CD8+ immunogenicity from the peptide and its
HLA-A/B/C allele with a small CNN (Li et al. 2021). It scores 9- and 10-mers
only and supports a fixed set of ~62 alleles, snapping anything else to the
nearest it knows. It emits one immunogenicity prediction per (peptide,
allele); score is in 0–1 (higher = more immunogenic). DeepImmuno ships its
weights in-repo and is MIT-licensed, but its script loads them with an old
Keras 2 / TensorFlow stack, so mhctools shells out to DeepImmuno's own CLI in a
separate checkout. Run mhctools fetch deepimmuno, or point at a manual clone
with DEEPIMMUNO_HOME, and set
DEEPIMMUNO_PYTHON to an interpreter that has TensorFlow (with Keras 2, or
newer TensorFlow plus the tf-keras shim — the wrapper sets
TF_USE_LEGACY_KERAS=1 for the subprocess).
from mhctools import DeepImmuno
DeepImmuno.fetch()
predictor = DeepImmuno(alleles=["HLA-A*02:01"]) # resolves DEEPIMMUNO_HOME / ~/DeepImmuno
results = predictor.predict(["NLVPMVATV", "GILGFVFTL"])
results[0].immunogenicity.score # 0.9568 (higher = more immunogenic)
⚠️ Every current CD8 immunogenicity predictor —
PRIME,BigMHC_IM, andDeepImmunoincluded — ranks well in the characterized regime but generalizes poorly to truly novel neoepitopes; independent benchmarks put the field near AUC 0.5–0.65 on unseen tumor neoepitopes (ITSNdb ~0.52–0.60, ICERFIRE ~0.56, IMPROVE ~0.60). In the one neutral head-to-head that scored both (NeoaPred, Bioinformatics 2024),BigMHC_IMedgedPRIMEon cancer neoepitopes, while PRIME tends to do better on viral / infectious-disease epitopes — its training positives are mostly viral and cancer-testis antigens, with only ~129 (v1) / ~596 (v2) true immunogenic neoepitopes. PRIME's higher self-reported numbers are partly attributable to documented train/test overlap (IMPROVE flagged ~70% overlap with its evaluation set). Use these scores to prioritize, not as ground truth.
TLimmuno2 is the odd one out: it predicts class-II (CD4+) immunogenicity —
the only class-II immunogenicity model here, filling a gap the class-I models
(Calis, PRIME, BigMHC_IM, DeepImmuno) leave. It scores a peptide against a class-II
allele (transfer-learned from class-II binding) and emits one immunogenicity
prediction per (peptide, allele): score in 0–1 (higher = more immunogenic)
and percentile_rank from its %Rank against a background set, rescaled to
0–100 (lower = more immunogenic). Native NetMHCIIpan-style keys (DRB1_0803,
HLA-DPA10103-DPB10101) pass through; common DR forms (HLA-DRB1*08:03) are
converted; anything TLimmuno2 does not know raises. Its upstream license is
ambiguous (an Apache-2.0 README badge, no LICENSE file), so mhctools does not
vendor it — it shells out to a user-provided checkout (TLIMMUNO2_HOME), with
TLIMMUNO2_PYTHON naming an interpreter that has TensorFlow (Keras 2, or newer
TensorFlow plus tf-keras).
from mhctools import TLimmuno2
predictor = TLimmuno2(alleles=["DRB1_0803"]) # resolves TLIMMUNO2_HOME / ~/TLimmuno2
results = predictor.predict(["FHTMWHVTRGAVLMY"])
results[0].immunogenicity.score # 0.9874 (higher = more immunogenic)
⚠️ TLimmuno2's %Rank is computed against ~90,000 background peptides per distinct allele, so a call costs about a minute per allele regardless of how many peptides you pass — batch peptides by allele. Class-II immunogenicity is noisier than class-I; a prioritization aid, not ground truth.
TCR specificity
| Predictor | Kinds produced | Requires |
|---|---|---|
NetTCR |
pMHC:TCR binding | mhctools fetch nettcr --accept-license + a TFLite runtime (pip install mhctools[nettcr]) |
MixTCRpred |
fixed-pMHC:TCR binding | pip install "mhctools[mixtcrpred]" + mhctools fetch mixtcrpred --accept-license |
NetTCR predicts whether a paired αβ T-cell receptor recognises a
(class-I) peptide. Unlike the MHC-ligand predictors, its input is a peptide
plus a TCR (the six CDR loops), not an allele, and it emits the
pMHC_TCR_binding kind. NetTCR ships its pretrained weights in its git
repository as small TFLite models; this wrapper runs the pan cross-validation
ensemble in-process and does not need NetTCR's conda environment.
from mhctools import NetTCR, TCR
NetTCR.fetch(accept_license=True) # downloads only the ~8 MB pan ensemble
predictor = NetTCR() # resolves NETTCR_DIR / ~/NetTCR-2.2
tcr = TCR(
cdr1a="NSASQS", cdr2a="VYSSG", cdr3a="VVEGDKVI",
cdr1b="MGHRA", cdr2b="YSYEKL", cdr3b="ASSHSGYEQF", name="clone1")
# Score explicit (peptide, TCR) pairs...
results = predictor.predict_pairs([("LLWNGPMAV", tcr)])
results[0].tcr_binding.score # ensemble-mean recognition probability
# ...or every peptide x TCR combination.
results = predictor.predict(["LLWNGPMAV", "GILGFVFTL"], [tcr])
Commandline examples
Prediction for user-supplied peptide sequences
mhctools --sequence SIINFEKL SIINFEKLQ --mhc-predictor netmhc --mhc-alleles A0201
--sequence may be repeated; all occurrences accumulate. Alternatively, use
--input-peptides-file for one peptide per line (blank lines are ignored), or
--input-fasta-file for protein sequences. Select exactly one of these three
input sources.
CLI prediction tables use a stable cross-predictor convention: plain peptide
inputs have an empty source_sequence_name and offset 0, while FASTA and
subsequence inputs retain their source and zero-based offset and are ordered by
those coordinates. prediction_method_name is the exact CLI predictor name
selected (including version and mode). affinity is IC50 in nM,
percentile_rank is a 0–100 percentile, and score remains
predictor-specific. CSV floats are serialized with six significant digits.
Automatically extract peptides as subsequences of specified length
mhctools --sequence AAAQQQSIINFEKL --extract-subsequences --mhc-peptide-lengths 8-10 --mhc-predictor mhcflurry --mhc-alleles A0201
Annotate an existing table with predictor scores (predict-table)
Downstream evaluation workflows often start from an annotated benchmark table
(with columns like sample_id, hit, peptide, and per-row genotype/allele
info) and just need external predictor scores appended. mhctools predict-table reads a CSV, runs each requested predictor once, and appends one
score column per predictor — choosing the best allele per row — while
preserving every input column:
mhctools predict-table \
--input benchmark.csv.bz2 \
--peptide-column peptide \
--alleles-column hla \
--predictor netmhcpan42-ba:netmhcpan4.2.ba:affinity \
--predictor netmhcpan42-el:netmhcpan4.2.el:score \
--out benchmark.with_scores.csv.bz2
Each --predictor spec is NAME[:OUTPUT_COLUMN[:FIELD]], where FIELD is
affinity, score, or percentile_rank (lower is better for affinity and
percentile_rank; higher for score). Rows may hold several alleles per cell
(whitespace-, comma-, or semicolon-separated); the best one per peptide is
chosen and recorded in a <OUTPUT_COLUMN>_best_allele provenance column.
Missing or blank peptide/allele cells remain unscored; they are never converted
to literal sequence or allele strings and sent to a predictor.
Pass --predictor-info info.csv to also write a sidecar describing each
column's score_field, units, and higher_is_better. Empty units means
the field is dimensionless or predictor-specific.
The same thing from Python (I/O-free, works on any DataFrame):
from mhctools import annotate_table, AnnotationSpec, NetMHCpan42_BA
annotated = annotate_table(
df,
[AnnotationSpec(
predictor=lambda alleles: NetMHCpan42_BA(alleles=alleles),
output_column="netmhcpan4.2.ba",
field="affinity")],
peptide_column="peptide",
allele_column="hla")
Legacy API
The old predict_peptides() and predict_subsequences() methods still work
and return BindingPredictionCollection objects:
predictor = NetMHCpan(alleles=["A*02:01"])
collection = predictor.predict_subsequences(
{"1L2Y": "NLYIQWLKDGGPSSGRPPPS"},
peptide_lengths=[9],
)
df = collection.to_dataframe()
for bp in collection:
if bp.affinity < 100:
print("Strong binder: %s" % bp)
To convert legacy results to the new types:
preds = collection.to_preds() # list of Prediction
pp_list = collection.to_peptide_preds() # list of PeptideResult
Per-bond peptidase evidence
DPP4qPISA evaluates the published human DPP4 N-terminal triplet model locally.
CleavageInput and CleavageResult preserve terminal chemistry, native scores,
assay provenance and parent-sequence bond coordinates. See the
cleavage guide for usage and model limits. qPISA scores
are substrate-depletion estimates, not serum half-lives or probabilities.
The local cleavage panel also provides motif rules for CPN, aminopeptidase P,
FAP, aminopeptidases A/N, DPP8/9, TPP2, puromycin-sensitive aminopeptidase,
PREP and ERAP2. Every motif rule states how strict it is — required,
preferred or permissive — together with the source observation behind that
grade, so a non-match can be read for what it is worth. Three antigen-processing
enzymes whose published specificity does not generalize (THOP1, neurolysin and
endosomal IRAP) ship as exact-sequence source references instead: they return
what an experiment reported for that precise chemical form and abstain on
anything else. An optional eramer-step model exposes ERAP1's existing
length-specific PWM score at the initial trimming bond. Discover models with
mhctools cleavage --list-models, or run
mhctools cleavage --sequence RPPGFSPFR --model app2-xp --model cpn-basic.
The guide includes a wider candidate inventory and
prioritized follow-up issues.
mhctools benchmark evaluates source-linked observations in separate assay,
endpoint and native-unit strata. It reports training overlap, repeated
measurements, unsupported inputs and missing target-domain evidence. See the
benchmark guide and mhctools benchmark --lineage-inventory.
Release files for mhctools 3.44.34
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mhctools-3.44.34.tar.gz | 406.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mhctools-3.44.34-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 695.6 kB
Release files / mhctools-3.44.34.tar.gz
| Download URL | mhctools-3.44.34.tar.gz |
|---|---|
| Size | 406.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c829aea965d356a973cf305250ef1276c4644d0ebcb7d8098a5a99446c169248
|
|
BLAKE2b-256 checksum How to use checksums |
831a1bab5d4580524dbf41b9c7a3bad442548636e98b7c9e3139d3aeac058471
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.6
|
Release files / mhctools-3.44.34-py3-none-any.whl
| Download URL | mhctools-3.44.34-py3-none-any.whl |
|---|---|
| Size | 289.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
baead5b9b083803a456b175e21fca6062120959d9c175f4195d58ded8fbac31e
|
|
BLAKE2b-256 checksum How to use checksums |
33d0d26a69f8b1471d4bdca9816f81f8d43a6a1ccbe31a6733dbee75976e62ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.6
|