The Federal Laws Corpus: Mexico’s Statute Book, One Text per Reform
How 3,724 dated reform texts of 315 federal instruments were assembled, and what can be trusted about them
Published
September 2, 2026
Abstract
Mexico’s federal legislation is easy to read today and hard to read historically. What is published is the text of each federal law as it stands right now, not the intermediate wording each reform left behind; the Diario Oficial de la Federación publishes each reform decree and nothing else, so the consolidated text of a law on a given date exists only as the sum of everything published before it. This page documents a corpus that closes that gap: 3,724 reform texts covering 315 of the 316 federal instruments in the project’s catalogue, each one the complete text of an instrument as it read after a particular reform, taken from the Supreme Court’s legislative database and, for most of them, linked back to the Diario Oficial provision that caused the change. We describe where the material comes from, how the collection is checked for completeness against the project’s own catalogue of federal laws, and how it is published and can be downloaded.
Open in Colab
Imports, the styling shared by every figure, and the aggregate summary this page reads
import jsonfrom pathlib import Pathfrom urllib.request import urlopenimport pandas as pdimport plotly.graph_objects as gofrom IPython.display import Markdown# Validated categorical palette (light mode), in fixed assignment order — the# same one `DOF Titles` and `Archive` use, so a colour means the same thing# across the site.BLUE, GREEN, MAGENTA, YELLOW ="#2a78d6", "#008300", "#e87ba4", "#eda100"LIGHT_BLUE ="#9ec5f4"# step 200 of the blue sequential rampINK, MUTED, GRID, AXIS ="#52514e", "#898781", "#e1e0d9", "#c3c2b7"PLOTLY_FONT ="system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif"def style_plotly(fig, *, yaxis_title="", height=430): fig.update_layout( font=dict(family=PLOTLY_FONT, size=13, color=INK), paper_bgcolor="white", plot_bgcolor="white", height=height, margin=dict(l=64, r=24, t=16, b=40), hoverlabel=dict(bgcolor="white", bordercolor=GRID, font=dict(color=INK, size=12)), legend=dict(font=dict(size=12, color=INK)), ) fig.update_xaxes( showgrid=False, showline=False, zeroline=False, ticks="", tickfont=dict(color=MUTED), title_text="", ) fig.update_yaxes( showgrid=True, gridcolor=GRID, gridwidth=1, showline=False, zeroline=False, ticks="", tickfont=dict(color=MUTED), title_text=yaxis_title, title_font=dict(color=INK, size=13), )return figPLOTLY_CONFIG = {"displaylogo": False,"displayModeBar": "hover","modeBarButtonsToRemove": ["lasso2d", "select2d", "autoScale2d"],}# Every figure and every number in the prose below comes from this one file.# The corpus itself lives in the `scjn-leyes` release — one tarball per# instrument — and is never committed to git, so, unlike `Archive`, which# queries the GitHub API live, this page reads the aggregate summary# `scripts/resume_scjn_leyes.py` distils from the crawl the release was# packaged from: counts, per-year totals and one row per instrument,# regenerated by re-running that script and versioned alongside the page.RESUMEN ="data/scjn-leyes-summary.json"RAW ="https://raw.githubusercontent.com/INGEOTEC/LegalIA/master/website/pages/"+ RESUMENlocal = Path(RESUMEN)if local.is_file(): # rendering the site resumen = json.loads(local.read_text(encoding="utf-8"))else: # running the notebook on Colabwith urlopen(RAW) as respuesta: resumen = json.loads(respuesta.read().decode("utf-8"))instrumentos = pd.DataFrame(resumen["instruments"])por_anio = pd.Series( {int(anio): n for anio, n in resumen["snapshots_per_year"].items()}).sort_index()# Headline figures, named once here and reused by the prose, the table and the# figure captions so no number on this page is typed by hand twice.SNAPSHOTS = resumen["snapshots"]CATALOGO = resumen["catalogue_entries"]CON_DIRECTORIO = resumen["instruments_with_directory"]ESTADOS = resumen["title_link_status"]POR_CONFIRMACION = resumen["title_link_status_by_confirmation"]ENLAZADOS = resumen["linked_by_title"]CONFIRMADOS = resumen["content_diff_confirmed"]# A date is identified when a same-day title match resolved it *or* when the# content diff resolved a date the title match left ambiguous — the two checks# are complementary, and this is the coverage figure that matters to a reader.RESCATADOS = POR_CONFIRMACION.get("ambiguous|confirmed", 0)IDENTIFICADOS = ENLAZADOS + RESCATADOS
Code
# On Colab, install the packages this notebook needs:# %pip install pandas plotly
1 What a reform text is
The unit of this collection is not a law and not a decree: it is a reform text — one plain-text file holding the complete text of one federal instrument as it read after one particular reform. The file is named after the date that reform took effect (01-04-2025.md) and lives in a directory named after the instrument (cpeum/ for the Constitution, lft/ for the Federal Labour Law), so an instrument’s directory is its own history laid out in files: as many files as there are dates on which its wording changed.
This matters because it is the version of the statute book that is otherwise missing. To know what the Federal Labour Law said in 1998 one can consult the 1998 reform decree in the Diario Oficial — but a decree only says which articles were replaced and by what, not what the resulting law read like as a whole, and for anything published before roughly 1999 that decree exists only as scanned images that have to be read by OCR before it can even be compared against anything. A reform text sidesteps all of that, because somebody has already done the consolidation: it is the law as of that date, in one piece.
The format is Markdown, and that choice is deliberate. Markdown is plain text, so the files are readable in any editor, searchable with grep, diffable line by line against each other, and stable in version control — the same reasons the rest of LegalIA converts legal texts to Markdown rather than keeping PDFs or .docx files around. It also keeps just enough structure to be useful — a heading is a heading, an emphasized run stays emphasized — without pretending to a legal-document schema the source never had. Each file opens with a short YAML header recording where it came from, which is described in detail below; everything after the header is the text of the law.
2 Why the Supreme Court’s legislative database
The Diario Oficial is the authoritative source, and it publishes reform decrees: each one says what changes, not what the law then reads. Consolidated current text of a federal law is published elsewhere, but only as it stands today — which tells you what a law says now and not what it said in between.
The Supreme Court’s legislative database (legislacion.scjn.gob.mx) is organized the other way around. For each instrument it keeps a table of reforms, and for each row of that table it serves the consolidated text of the instrument as it stood after that reform — a full document, not a summary of the change. The Court maintains this because its own work requires it: a case is decided under the law in force at the relevant time, not under the law in force today. That editorial effort, undertaken for judicial reasons, is exactly what a historical corpus needs, and it is why the Court is this collection’s primary source of content. Its scope — which instruments exist, what each is called, and when each was last reformed — comes from the collection’s own published index plus the gazette itself, described in the next section.
2.1 A caveat about the source
The Court’s database is a source of convenience, not the source of authority. The authoritative text of Mexican federal law is what the Diario Oficial de la Federación published; the Court’s consolidated versions are an editorial product built on top of it, and they behave like one:
Instruments are addressable, individual searches are not. The Court’s own JSON API gives each instrument a stable identifier, recorded in every file’s header, so a later crawl can go straight back to it instead of searching again. What still has no citable address is a search itself — there is no results page to link to — so the only reproducible record of how an instrument was first found is what was searched for and what came back, which is why the header keeps that pair.
The search can return the wrong document entirely. A query for one law can rank a different law, or an internal court agreement that merely mentions the law, above the law itself. Two mechanisms guard against this: a similarity score between what was searched and what was found, recorded in every file, and a filter that discards the Court’s own internal agreements. Neither is infallible, which is why publication of this collection is a manual act, described at the end of this page.
Editorial matter is mixed into the text. The Court annotates its consolidated texts with editor’s notes (“N. DE E.”) explaining, for example, that a reform was later invalidated. These are the Court speaking, not the legislature, and are stripped at conversion time so they never masquerade as statutory text.
Because the source is editorial, every file declares its provenance in its own header — fuente: scjn — and nothing in LegalIA ever silently mixes these texts with Markdown built from the Diario Oficial itself.
3 From the two sources to one corpus
The pipeline that builds the collection has four stages, run in order, each one a separate script under scripts/ (Figure 1). What follows describes not only what each stage does but what it decides, since the decisions are where a corpus of this kind either earns or loses a reader’s trust.
flowchart TD
D[scjn-leyes release<br/>indice-global.json.gz] -->|nombre, abrev fallback| C[estado.json per law<br/>fetch_scjn_legislacion.py --plan]
C -->|search by nombre| S[SCJN SCOW API<br/>fetch_scjn_legislacion.py]
S -->|BusquedaFrase, Reforma, Articulos| M[Markdown texts<br/>DD-MM-YYYY.md + YAML header]
A[notas-archivo release] -->|codNota + titulo + fecha| T[titles stream<br/>legal_provisions_titles]
M --> L[enlaza_scjn_legislacion.py]
T --> L
N[dofjson.get_nota<br/>DOF Markdown] --> L
L -->|indice.json per instrument| P[empaqueta_scjn_leyes.py<br/>one slug.tgz + MANIFEST.md]
P -.->|manual review, manual upload| R[(scjn-leyes release)]
Figure 1: The pipeline. Two things enter — the catalogue of instruments, read back off the collection’s own release, and the DOF’s archive of published provision titles — and one corpus of dated reform texts, each linked to the provision that produced it, comes out. The catalogue’s own actualizado is the newer of the Court’s reform table and those same DOF titles. The dotted arrow is the only step no script performs.
3.1 Seeding: which instruments exist
The first stage answers one question — which instruments exist? — and keeps three fields per entry: the instrument’s official nombre, its short slug (abrev, which becomes the directory name), and actualizado, the date of its most recent reform. It asks no third-party catalogue for any of them. The list of instruments is read back out of the collection’s own release: the published indice-global.json.gz already carries every law’s slug and name, so the seed is re-read rather than rebuilt. actualizado is the newer of two independent answers — the Court’s own reform table and the newest DOF provision whose title names the law — because each misses what the other sees: the gazette alone under-dates a law reformed by an omnibus decree that names none of them, and the Court alone freezes a law it has not indexed yet. A law neither can date carries no actualizado at all, which the refresh planner reads as “always re-check”.
A federal law nobody has entered yet is found rather than waited for: the Court’s catalogue of laws and codes is listed and diffed against the catalogue, each candidate is confirmed against a DOF title that names it, and the survivors are reported, not added — a new federal law is a handful a year, and its abrev is a release asset name, so adding one is a human’s decision.
actualizado earns its place by making refreshes cheap. Once the whole collection has been crawled start to finish, a later run can skip an instrument that has not been reformed since — but only if it already has reform texts on disk. An instrument with none is retried on every single refresh, forever, which is what makes the collection self-healing for laws the Court has not indexed yet: when the Court catches up, the next refresh picks the law up with nothing to configure by hand.
3.2 Crawling: finding the right document
The second stage searches the Court’s own JSON API for each catalogue name — a relevance-ranked list of documents comes back, and the crawler has to decide which one, if any, is the instrument it was looking for. It discards the Court’s own internal Pleno agreements outright, since one of those merely mentioning a law in passing is not the law: a name search for the Income Tax Law once surfaced only such agreements, no actual law, which is the failure mode this guard exists for. It also rejects a candidate of the wrong kind — a reglamento is never the ley it regulates, even when its title does not say so — keeps only federal, currently-in-force instruments when any exist, and picks the highest-ranked survivor by name similarity. The winner still has to clear a minimum similarity floor to be accepted at all, and is flagged sospechoso (suspect) when it clears that floor but not a stricter confidence one — a signal for manual review, not a rejection. For the winner, the crawler walks its reform table through the API and converts each reform’s own text to Markdown, stripping the editorial notes.
A law the API does not index at all is a different failure mode from a wrong match, and it used to be a real gap: a search for the LEY Federal de Cine y el Audiovisual once returned nothing at all, twice, live. The fix was not a smarter query — an instrument is now addressed by the Court’s own stable identifier once it is indexed, and the name search is simply retried on every refresh until then, since a brand-new law can be indexed at any time with no signal that it happened.
The stage is resumable at two levels — a file already on disk is never re-downloaded, and the index of the last instrument attempted is checkpointed, so an interrupted run resumes rather than restarts — and rate-limited, out of courtesy to an unofficial service that was never designed to be crawled.
3.3 The header as a provenance record
Every reform text opens with a YAML header. It is short, and each line is there to answer a question a sceptical reader would ask:
---fuente: scjnnombre_buscado: CONSTITUCIÓN Política de los Estados Unidos Mexicanosordenamiento: CONSTITUCION POLITICA DE LOS ESTADOS UNIDOS MEXICANOSfecha_publicacion: 01-04-2025fecha_expedicion: 01-04-2025categoria: DECRETOratio_similitud:1.000sospechoso:false---
fuente — where the text came from: scjn for the Court’s consolidated version, dof for the handful of files taken directly from the Diario Oficial, when the Court has not indexed an instrument yet. This is the field that keeps an editorial text from ever being mistaken for the official one.
ordenamiento — the title of the document the Supreme Court actually served, verbatim.
nombre_buscado — the exact string that was searched for, which is the instrument’s name as the catalogue gives it (the nombre seeded in the first stage) or, for the one entry that needs it, the nombre_scjn override described below. The pair is therefore not only what was asked / what came back: it is the catalogue’s name for the law against the Court’s own name for the same law, which is why the two can differ at all and why the similarity score below is worth recording. Because an individual search has no citable address of its own, the pair is also the only way to reproduce by hand how a given file was reached. It is written only when the two differ after normalizing accents, case and whitespace — so its mere presence marks a file whose identification deserves a second look, and grep -l nombre_buscado: is a complete audit.
ratio_similitud and sospechoso — how close those two strings are (1.000 meaning identical after normalization) and whether that closeness fell into the band where a match is kept but flagged. They record the crawler’s confidence rather than hiding it behind a yes/no decision.
fecha_publicacion and fecha_expedicion — the date the reform was published and the date it was issued, as the Court’s reform table gives them; the first is the reform text’s identity and its file name.
categoria — the kind of instrument that produced the reform, typically DECRETO.
A file crawled through the Court’s JSON API also carries the instrument’s own stable id_ordenamiento and a reforma_id for this specific reform — the pair that lets a later run address it directly instead of searching by name again.
The header plays the same role here that the raw JSON record of a note plays on the Archive page: it is the level at which a reader can check the data rather than take it on faith.
3.4 Linking each text to the provision that caused it
The Court’s database does not say which Diario Oficial provision produced a given reform text. A last stage adds that link where it can: for a text, it first looks at what the gazette published that same day, and — when several same-day provisions all plausibly name the instrument — falls back to comparing the candidates’ own text against what actually changed. The result is one indice.json per instrument, pairing each text with a codNota, the DOF’s own identifier for the provision, whenever the pipeline is confident enough to commit to one.
This is the most laborious part of the whole pipeline, and it is still changing, so a full account of how it decides is deliberately left for when it settles. Figure 3 and Table 1 below show what it currently achieves, in numbers rather than mechanism; the link itself is what makes a reform text checkable against the official record, joinable with the rest of LegalIA by codNota, and orderable on days the gazette published more than one reform to the same law.
4 What the corpus looks like today
Table 1 gives the size of the collection as it stands. Every figure on this page is computed from the summary file described in the methodological note, not typed in by hand.
Code
filas = [ ("Instruments in the catalogue", f"{CATALOGO:,}", ""), ("With a directory of reform texts", f"{CON_DIRECTORIO:,}",f"{CON_DIRECTORIO / CATALOGO *100:.1f}% of the catalogue"), ("Markdown reform texts", f"{SNAPSHOTS:,}", ""), ("Taken from the SCJN", f"{resumen['snapshots_by_source'].get('scjn', 0):,}",f"{resumen['snapshots_by_source'].get('scjn', 0) / SNAPSHOTS *100:.1f}%"), ("Taken directly from the DOF (Court has not indexed the instrument yet)",f"{resumen['snapshots_by_source'].get('dof', 0):,}",f"{resumen['snapshots_by_source'].get('dof', 0) / SNAPSHOTS *100:.2f}%"), ("Identified by a same-day title match alone", f"{ENLAZADOS:,}",f"{ENLAZADOS / SNAPSHOTS *100:.1f}%"), ("Ambiguous dates resolved by the content-diff check", f"{RESCATADOS:,}",f"{RESCATADOS / SNAPSHOTS *100:.1f}%"), ("Identified either way", f"{IDENTIFICADOS:,}",f"{IDENTIFICADOS / SNAPSHOTS *100:.1f}%"), ("Confirmed by content diff (any state)", f"{CONFIRMADOS:,}",f"{CONFIRMADOS / SNAPSHOTS *100:.1f}%"), ("Still unidentified", f"{SNAPSHOTS - IDENTIFICADOS:,}",f"{(SNAPSHOTS - IDENTIFICADOS) / SNAPSHOTS *100:.1f}%"), ("Years covered", f"{por_anio.index.min()}\u2013{por_anio.index.max()}", ""),]Markdown("\n".join( ["| | | |", "|---|---:|---|"]+ [f"| {etiqueta} | {valor} | {nota} |"for etiqueta, valor, nota in filas]))
Table 1: The federal laws corpus at a glance. Percentages of reform texts are over the 3,724 total.
Instruments in the catalogue
316
With a directory of reform texts
315
99.7% of the catalogue
Markdown reform texts
3,724
Taken from the SCJN
3,722
99.9%
Taken directly from the DOF (Court has not indexed the instrument yet)
2
0.05%
Identified by a same-day title match alone
2,474
66.4%
Ambiguous dates resolved by the content-diff check
793
21.3%
Identified either way
3,267
87.7%
Confirmed by content diff (any state)
2,634
70.7%
Still unidentified
457
12.3%
Years covered
1889–2026
4.1 How deep the history goes
Figure 2 plots the reform texts by the year of the reform that produced them, and it is the clearest available picture of when Mexico’s federal statute book actually changed. Three things stand out. The first half-century is thin: the Court’s database reaches back to 1917 and beyond — the oldest text in the collection is the Commercial Code of 1889, older than the Diario Oficial coverage this project is built on — but reforms before the 1970s are recorded in ones and tens per year, partly because there were fewer and partly because the Court’s retrospective coverage of them is incomplete. Volume then rises through the last quarter of the century and settles, from the 1990s onward, into a regime of well over a hundred reforms a year across the collection. The final year is partial: it covers only the months already elapsed when the crawl ran.
Code
ultimo = por_anio.index.max()colores = [LIGHT_BLUE if anio == ultimo else BLUE for anio in por_anio.index]fig = go.Figure( go.Bar( x=por_anio.index, y=por_anio.values, marker=dict(color=colores, cornerradius=2), hovertemplate="%{x}: %{y:,} reform texts<extra></extra>", ))style_plotly(fig, yaxis_title="Reform texts")fig.update_layout(bargap=0.2, showlegend=False, hovermode="x unified")fig.add_annotation(x=ultimo, y=por_anio.iloc[-1], text="partial year", yshift=14, showarrow=False, font=dict(color=MUTED, size=11))fig.show(config=PLOTLY_CONFIG)
Figure 2: Reform texts per year of publication. The last year, in a lighter shade, is partial — it ends when the crawl ran. Hover on a bar for the exact count.
4.2 How much of it is identified, and what the content-diff check adds
Figure 3 is the figure to read before using this collection for anything. It breaks the 3,724 reform texts down by the state a same-day title match left them in, and splits each state by whether the content-diff check confirmed a provision. The stacked pair is the point: the ambiguous bar’s confirmed segment counts dates the title match could not resolve and the content-diff check could — the concrete return on the more expensive computation.
Code
orden = ["linked", "ambiguous", "claimed", "none"]etiquetas = {"linked": "linked<br/>(one candidate)","ambiguous": "ambiguous<br/>(several candidates)","claimed": "claimed<br/>(taken by another text)","none": "none<br/>(no candidate that day)",}confirmados = [POR_CONFIRMACION.get(f"{e}|confirmed", 0) for e in orden]sin_confirmar = [POR_CONFIRMACION.get(f"{e}|unconfirmed", 0) for e in orden]fig = go.Figure()fig.add_bar( x=[etiquetas[e] for e in orden], y=confirmados, name="confirmed by content diff", marker=dict(color=BLUE), hovertemplate="%{y:,} reform texts<extra>confirmed</extra>",)fig.add_bar( x=[etiquetas[e] for e in orden], y=sin_confirmar, name="not confirmed", marker=dict(color=LIGHT_BLUE), hovertemplate="%{y:,} reform texts<extra>not confirmed</extra>",)style_plotly(fig, yaxis_title="Reform texts", height=440)fig.update_layout(barmode="stack", bargap=0.35, hovermode="x unified", legend=dict(orientation="h", y=1.12, x=0))fig.update_yaxes(tickformat=",")fig.show(config=PLOTLY_CONFIG)
Figure 3: Reform texts by title-match state, split by whether the content diff confirmed a provision. The confirmed part of the ambiguous bar is what the content-diff check recovers on its own.
The two sentences of the paragraph below, computed rather than written
amb = ESTADOS.get("ambiguous", 0)sin_ident = {int(a): n for a, n in resumen["unidentified_per_year"].items()}SIN_IDENT_TOTAL =sum(sin_ident.values())RECIENTES =sum(n for a, n in sin_ident.items() if a >=2000)ANTIGUOS =sum(n for a, n in sin_ident.items() if a <1990)Markdown(f"Read together, the title match and the content-diff check identify "f"{IDENTIFICADOS:,} of the {SNAPSHOTS:,} reform texts "f"({IDENTIFICADOS / SNAPSHOTS *100:.1f}%): "f"{ENLAZADOS:,} where a single same-day provision named the instrument, plus "f"{RESCATADOS:,} of the {amb:,} ambiguous dates where the content diff picked "f"one candidate out of several. That leaves {SNAPSHOTS - IDENTIFICADOS:,} "f"reform texts ({(SNAPSHOTS - IDENTIFICADOS) / SNAPSHOTS *100:.1f}%) whose "f"originating provision is still unknown, and — against intuition — these are not "f"mostly the oldest ones: {RECIENTES:,} of them ({RECIENTES /max(SIN_IDENT_TOTAL, 1) *100:.0f}%) "f"fall in 2000 or later, and only {ANTIGUOS:,} before 1990. The reason is that the "f"unresolved cases are dominated by days on which the gazette published several "f"provisions all naming the same law — a modern habit, since a single reform "f"package now often arrives as several decrees — and the content diff was not "f"decisive between them. The genuinely old failures are of the other kind: "f"{ESTADOS.get('none', 0):,} reform texts have no same-day candidate at all. "f"Note also that confirmation is not the same as agreement: of the "f"{ENLAZADOS:,} title-matched reform texts, "f"{POR_CONFIRMACION.get('linked|confirmed', 0):,} were independently confirmed by "f"content, and the rest simply had no digital candidate text to check against.")
Read together, the title match and the content-diff check identify 3,267 of the 3,724 reform texts (87.7%): 2,474 where a single same-day provision named the instrument, plus 793 of the 1,166 ambiguous dates where the content diff picked one candidate out of several. That leaves 457 reform texts (12.3%) whose originating provision is still unknown, and — against intuition — these are not mostly the oldest ones: 268 of them (59%) fall in 2000 or later, and only 135 before 1990. The reason is that the unresolved cases are dominated by days on which the gazette published several provisions all naming the same law — a modern habit, since a single reform package now often arrives as several decrees — and the content diff was not decisive between them. The genuinely old failures are of the other kind: 68 reform texts have no same-day candidate at all. Note also that confirmation is not the same as agreement: of the 2,474 title-matched reform texts, 1,839 were independently confirmed by content, and the rest simply had no digital candidate text to check against.
4.3 Instrument by instrument
Table 2 lists the twenty-five instruments with the most reform texts, which is a reasonable proxy for the most heavily reformed pieces of Mexican federal law. It is the same information the collection’s MANIFEST.md carries — the file a human reads in full before publishing — reordered by size rather than by confidence.
Two columns beyond the instrument’s own name and slug are worth explaining. Reform texts is how many dated versions of that instrument the collection holds, and therefore how many times its text changed within the Court’s coverage; the Constitution leads by a wide margin, which is itself a substantive fact about Mexican constitutionalism. Span gives the first and last year covered, which is where the Court’s retrospective reach shows: an instrument may be older than its span suggests. How well each instrument’s own texts are individually tied back to the Diario Oficial is a separate question from how many it has, and is not broken out here — Figure 3 above answers it for the collection as a whole.
Code
TOPN =25tabla = instrumentos.head(TOPN)lineas = ["| Instrument | Slug | Reform texts | Span |","|---|---|---:|---|",]for fila in tabla.itertuples(): span = (f"{fila.first_year}\u2013{fila.last_year}"if fila.first_year and fila.last_year and fila.first_year != fila.last_yearelse (str(fila.first_year) if fila.first_year else"\u2014") ) nombre = fila.nombre iflen(fila.nombre) <=70else fila.nombre[:67] +"\u2026" lineas.append(f"| {nombre} | `{fila.slug}` | {fila.snapshots:,} | {span} |")Markdown("\n".join(lineas))
Table 2: The twenty-five instruments with the most reform texts.
Instrument
Slug
Reform texts
Span
CONSTITUCIÓN Política de los Estados Unidos Mexicanos
cpeum
300
1917–2026
CÓDIGO Penal Federal
cpf
166
1931–2026
LEY General de Salud
lgs
146
1984–2026
CÓDIGO Fiscal de la Federación
cff
118
1981–2026
LEY Federal de Derechos
lfd
92
1981–2025
LEY Orgánica de la Administración Pública Federal
loapf
82
1976–2026
CÓDIGO de Comercio
ccom
74
1889–2026
CÓDIGO Civil Federal
ccf
62
1928–2025
LEY del Impuesto Especial sobre Producción y Servicios
lieps
60
1980–2025
LEY Federal de Protección al Consumidor
lfpc
57
1992–2025
LEY Federal del Trabajo
lft
55
1970–2026
LEY del Impuesto al Valor Agregado
liva
54
1978–2021
LEY de Instituciones de Crédito
lic
53
1990–2025
LEY General del Equilibrio Ecológico y la Protección al Ambiente
lgeepa
53
1988–2026
LEY de Vías Generales de Comunicación
lvgc
51
1940–2025
LEY Orgánica del Congreso General de los Estados Unidos Mexicanos
locg
47
1999–2025
LEY del Seguro Social
lss
44
1995–2026
LEY Monetaria de los Estados Unidos Mexicanos
lmeum
44
1931–2026
LEY Aduanera
ladua
40
1995–2025
LEY de Coordinación Fiscal
lcf
40
1978–2024
REGLAMENTO de la Cámara de Diputados
reg-diputados
40
2010–2025
LEY Federal del Impuesto sobre Automóviles Nuevos
lfisan
34
1996–2025
LEY del Instituto del Fondo Nacional de la Vivienda para los Trabaj…
lifnvt
31
1972–2025
LEY Federal de los Trabajadores al Servicio del Estado, Reglamentar…
lftse
31
1963–2026
REGLAMENTO del Senado de la República
reg-senado
31
2010–2024
The distribution behind the table’s long tail
un_solo =int((instrumentos["snapshots"] ==1).sum())todo_enlazado =int((instrumentos["snapshots"] == instrumentos["linked"]).sum())sin_enlace =int((instrumentos["linked"] ==0).sum())mediana =int(instrumentos["snapshots"].median())Markdown(f"Beyond the top of the table the distribution is very uneven: the median "f"instrument has {mediana} reform texts, {un_solo} of the {CON_DIRECTORIO} "f"instruments have exactly one — laws never reformed since they were enacted, "f"or enacted so recently that the Court's table holds a single row — "f"{todo_enlazado} have every one of their reform texts resolved by a same-day "f"title match alone, and {sin_enlace} have none resolved by it at all.")
Beyond the top of the table the distribution is very uneven: the median instrument has 5 reform texts, 66 of the 315 instruments have exactly one — laws never reformed since they were enacted, or enacted so recently that the Court’s table holds a single row — 119 have every one of their reform texts resolved by a same-day title match alone, and 9 have none resolved by it at all.
5 Every federal law is in the release
The catalogue lists 316 federal instruments; 315 have a directory in the collection today. The one gap, the General Ordinance of the Navy (oga), was published between 1 and 8 January 1912 — five years before the Diario Oficial’s own coverage begins in 1917 — and the Court does not index it either, so there is nothing in either source to build it from. Every instrument the two sources between them can date, this collection has.
Completeness here is not asserted, it is checked, and the check is cheap because of how the release is shaped: one tarball per catalogue entry, named after its abrev, and nothing else. So “does the release have every law” reduces to comparing two lists — the catalogue’s own abrev column against the tarballs the release actually publishes. That same list of tarballs is what a downstream reader walks to use the collection: nota2md’s iter_current_federal_laws() opens one at a time and yields the current text of the law inside, so consuming the whole collection and confirming it is complete are the same pass over the data, not two.
from nota2md import iter_current_federal_lawstexts =list(iter_current_federal_laws())len(texts) # one per instrument the release currently publishes a tarball for
6 How this collection is published
The corpus is published, since August 2026, as the scjn-leyes GitHub release.
One reproducible tarball per instrument. The packaging step writes one <slug>.tgz for each instrument rather than a single archive for the whole collection, because a consumer almost always wants one law, and a later run then has to re-upload only the laws that changed. Every member is prefixed with the instrument’s slug, so a tarball unpacks anywhere without stepping on anything:
<slug>/<date>.md the reform texts, each with its provenance header
<slug>/indice.json the codNota link and how it was decided
<slug>/notas/nota-<cod>.md the DOF text of every candidate considered
The gazette notes travel with the reform texts on purpose: they are the text each link was decided against, so shipping both makes a link auditable without going back to the network. Two small text assets accompany the tarballs — a MANIFEST.md ranking every instrument by confidence, and a SHA256SUMS.txt. Each tarball is byte-reproducible: gzip is stamped with a zero timestamp, members are added in sorted order, and their times, modes and ownership are fixed. Identical data therefore yields an identical file, which is what makes it possible to tell an unchanged instrument from a changed one by comparing bytes rather than guessing, which is what makes “re-upload only the laws that changed” a byte comparison instead of a guess.
Published as GitHub release assets, like the DOF notes archive, and for the same reason: data is never committed to this repository.
Published by hand, always, by design. This is the one operational difference between this collection and the others, and it is a statement about the source rather than a gap in automation. The notes archive is rebuilt and republished monthly by a workflow with no human in the loop, because its source is the gazette’s own service and an error there is the gazette’s error. The Court’s search, by contrast, can return a completely wrong document, and a wrong document would enter the corpus looking exactly like a right one — so the packaging script never invokes gh, no workflow may publish this collection, and MANIFEST.md is read in full before anything is uploaded. Separating packaging from publishing keeps the human judgement where the risk actually is.
Reading it back. Because one instrument is one asset, downloading a law needs no library at all:
Reading the corpus from Python is a call rather than an exercise: nota2md.download_scjn_leyes_corpus("lft") gives one law’s reform texts and their codNota links, download_scjn_leyes_index() the reverse index codNota → (law, reform text), download_scjn_leyes_catalog() the catalogue of every law the release publishes, and iter_current_federal_laws() the current text of every law, one at a time, without loading any of their history.
Licensing and attribution are still open. The reform texts are the Supreme Court’s editorial work over public legal texts, and the terms on which they may be redistributed have yet to be settled; this page is where the answer will be recorded. Nothing about the pipeline depends on that answer — the scripts reconstruct the corpus from the sources on any machine — but what the release carries does.
Methodological note
The material described here is built by four scripts in scripts/ — discover_federal_laws.py, fetch_scjn_legislacion.py, enlaza_scjn_legislacion.py and empaqueta_scjn_leyes.py — plus a small number of single-law scripts for instruments the Court does not index yet (e.g. fetch_lfiiedb_dof.py), whose text is taken directly from the DOF instead. The SCJN-specific logic they share lives in its own package, scjn — the transport (scjn.api) and the disk-first release readers (scjn.release); the Diario Oficial side comes from dofjson, and nota2md links a snapshot to the DOF codNota that produced it.
The corpus is published as release assets and is never committed to git, so this page does not read it directly — downloading 315 tarballs to draw two figures would be a poor trade. It reads data/scjn-leyes-summary.json, an aggregate summary produced by scripts/resume_scjn_leyes.py from the crawl that the August 2026 release was packaged from: headline counts, reform texts per year, the title-match states crossed with content confirmation, and one row per instrument. That script reuses the very function empaqueta_scjn_leyes.py uses to build MANIFEST.md, so the numbers on this page, the numbers a human reads before publishing, and the manifest shipped with the release cannot drift apart. Regenerating the summary after a fresh crawl is one command:
This page was written by the LegalIA team together with Claude, Anthropic’s coding assistant, through Claude Code: the assistant implemented the summary step, produced the figures and drafted the accompanying text. The authors verified the numbers against the corpus on disk and are responsible for the interpretations advanced here.