Skip to main content

gtfslint

Fast, pure-Python GTFS Schedule validator and quality report that runs in CI.

gtfslint checks a GTFS Schedule feed (a .zip archive or a folder) against the specification, reports every problem with a stable rule code, a severity and the exact file/line/field, and computes quality metrics such as service span, trips per day and headways. It is a pip install away, has no compiled dependencies, and validates a feed with one million stop_times.txt rows in about seven seconds.

The canonical MobilityData GTFS Validator is a Java application. gtfslint does not replace it. It covers the checks that catch most broken feeds and fits where a Python tool works better: pre-commit hooks, GitHub Actions, data pipelines and notebooks. When a rule matches a MobilityData notice, the notice name is listed next to it, so you can compare the two reports.

Installation

pip install gtfslint-cli

The distribution on PyPI is called gtfslint-cli (the name gtfslint is too close to an existing project); the import name and the command are both gtfslint.

Python 3.9 or newer. The only dependencies are tomli on Python < 3.11 and tzdata on Windows.

Command line

gtfslint check feed.zip                       # text report, exit code 1 on errors
gtfslint check feed.zip --html report.html    # also write a self-contained HTML report
gtfslint check feed/ --format json > report.json
gtfslint check feed.zip --format github       # GitHub Actions annotations
gtfslint check feed.zip --select GL2,GL4 --ignore GL408 --fail-on warning
gtfslint stats feed.zip --html stats.html     # quality metrics
gtfslint rules                                # list every rule

check options:

Option Meaning
-f, --format text (default), json, markdown, html, github
-o, --output FILE write the --format output to a file instead of stdout
--json / --markdown / --html FILE write extra reports in those formats
--select, --ignore comma-separated rule codes, code prefixes (GL4) or names (invalid_url)
--fail-on lowest severity that fails: error (default), warning, info, never
--reference-date date used by the expiration rules (default: today)
--max-notices-per-rule notices kept per rule (default 1000; counts are always complete)
--stats add feed statistics to the report
-q, --quiet print only the summary line
--config FILE, --no-config choose or skip the pyproject.toml configuration

Exit codes: 0 when no notice reaches --fail-on, 1 when at least one does, 2 for usage errors, invalid configuration or an unreadable feed.

GitHub Actions

- run: pip install gtfslint-cli
- run: gtfslint check gtfs/ --format github --html gtfs-report.html
- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: gtfs-report
    path: gtfs-report.html

When the feed is a folder, annotations point to the exact file and line. When it is a zip archive, they are attached to the archive and the message gives the location inside it.

pre-commit

repos:
  - repo: https://github.com/YusufEminoglu/gtfslint_sdk
    rev: v0.1.0
    hooks:
      - id: gtfslint

The hook runs gtfslint check on every committed .zip feed.

Configuration

Settings are read from the nearest pyproject.toml. Command-line options override them.

[tool.gtfslint]
select = ["ALL"]               # codes, prefixes or names
ignore = ["GL401", "GL606"]
fail-on = "warning"            # error | warning | info | never
max-notices-per-rule = 1000
reference-date = "2026-11-08"  # default: today
min-stop-distance-m = 2.0      # GL502 threshold
max-parent-distance-m = 1000.0 # GL504 threshold
max-stop-distance-km = 50      # GL503 threshold for every route type (default: per type)
speed-limits-kmh = { 3 = 120, 0 = 80 }   # GL408 limits per basic route_type

Python API

import gtfslint

report = gtfslint.validate("feed.zip")          # -> gtfslint.Report
print(report.errors, report.warnings, report.infos, report.ok)
for notice in report.notices:
    print(notice.code, notice.severity.label, notice.location, notice.message)

report.write("report.html")                     # format inferred from the suffix
data = report.to_dict()                         # also to_json/to_text/to_markdown/to_github
report.exit_code(fail_on="warning")

report = gtfslint.validate(
    "feed/",
    select=["GL2", "GL4"],
    ignore=["fast_travel_between_consecutive_stops"],
    reference_date="2026-11-08",
    with_stats=True,
)

st = gtfslint.stats("feed.zip")                 # -> gtfslint.FeedStats
st.trips_per_day["2026-11-09"], st.busiest_date, st.headways[0].median_headway_min
cfg = gtfslint.load_config("pyproject.toml")    # -> gtfslint.Config
gtfslint.validate("feed.zip", config=cfg)

gtfslint.RULES maps every code to a Rule, which has code, name, severity, description, reference and mobilitydata. gtfslint.FeedError is raised when the path does not exist or is not a readable zip archive.

Quality metrics

gtfslint stats and gtfslint.stats() report:

  • Counts: agencies, stops, stations, entrances, generic nodes, boarding areas, routes, trips, stop_times, shapes, services, frequency-based trips.
  • Route types: number of routes per type. Extended types are listed separately.
  • Service span: first and last service date, the number of active dates, and the first departure and last arrival times. Times past 24:00:00 are kept as they are.
  • Trips per day: trip count for each service date, after combining calendar.txt and calendar_dates.txt. Trips in frequencies.txt are expanded into their departures. The busiest date is reported separately.
  • Headways per route and direction: minimum, median, mean and maximum headway in minutes, and the number of departures, on the busiest date or on --date. Vehicle-hours for the same date.
  • Coverage: bounding box of stops and shape points (lon/lat).
  • Shapes: percentage of trips whose shape_id resolves to a shape.
  • Completeness: percentage of stops with wheelchair_boarding, trips with wheelchair_accessible, trips with bikes_allowed (value 1 or 2), and stops with coordinates.

Rules

Every rule has a code (GL + three digits), a snake_case name, a default severity and a link to the part of the GTFS Schedule reference it enforces. The last column gives the matching MobilityData validator notice, when there is one. Row numbers are physical line numbers, with the header on line 1.

Code Name Severity Description MobilityData notice
GL001 missing_required_file error A file required by the specification (agency, routes, trips, stop_times, stops) is missing. missing_required_file
GL002 missing_calendar_and_calendar_date_files error Neither calendar.txt nor calendar_dates.txt is present; at least one is required. missing_calendar_and_calendar_date_files
GL003 empty_file error A required file is empty or has a header but no records. empty_file
GL004 missing_required_column error A required column is absent from the header of a file. missing_required_column
GL005 duplicated_column error The same column name appears more than once in a header. duplicated_column
GL006 empty_column_name error A header contains an empty column name. empty_column_name
GL007 invalid_row_length error A row has a different number of fields than the header. invalid_row_length
GL008 csv_parsing_failed error The file could not be parsed as UTF-8 CSV; the rest of the file was skipped. csv_parsing_failed
GL009 unknown_file info The feed contains a file that is not part of the GTFS Schedule specification. unknown_file
GL010 unknown_column info A file has a column that is not defined by the specification. unknown_column
GL011 leading_or_trailing_whitespaces warning A value or column name has leading or trailing whitespace (it is trimmed before the other checks). leading_or_trailing_whitespaces
GL012 duplicate_key error Two records share the same primary key (e.g. stop_id, or trip_id + stop_sequence). duplicate_key
GL013 missing_required_field error A required or conditionally required field is empty. missing_required_field
GL014 route_both_short_and_long_name_missing error A route has neither route_short_name nor route_long_name. route_both_short_and_long_name_missing
GL015 empty_row warning A row contains only empty fields. empty_row
GL016 missing_recommended_file warning feed_info.txt is missing; it is recommended so consumers know the publisher and validity window. missing_recommended_file
GL101 invalid_color error A color is not a six-digit hexadecimal value such as FFFFFF. invalid_color
GL102 invalid_url error A URL is not a fully qualified http:// or https:// URL. invalid_url
GL103 invalid_timezone error A timezone is not a valid IANA TZ database name such as Europe/Istanbul. invalid_timezone
GL104 invalid_date error A date is not a valid calendar date in YYYYMMDD format. invalid_date
GL105 invalid_time error A time is not in H:MM:SS or HH:MM:SS format (hours above 24 are allowed). invalid_time
GL106 invalid_integer error A value that must be an integer (or enumeration) could not be parsed. invalid_integer
GL107 invalid_float error A value that must be a number could not be parsed. invalid_float
GL108 unexpected_enum_value warning An enumeration field has a value not listed by the specification. unexpected_enum_value
GL109 number_out_of_range error A number is outside its allowed range (latitude, longitude, non-negative or positive fields). number_out_of_range
GL110 invalid_email error An e-mail address is malformed. invalid_email
GL111 invalid_language_code error A language code is not a well-formed IETF BCP 47 tag. invalid_language_code
GL112 invalid_currency error A currency code is not an ISO 4217 alphabetical code. invalid_currency
GL113 point_near_origin error A stop or shape point lies at (0, 0) ('null island'), usually a missing coordinate. point_near_origin
GL114 inconsistent_agency_timezone error Agencies in agency.txt use different agency_timezone values. inconsistent_agency_timezone
GL115 route_color_contrast warning route_color and route_text_color have a WCAG contrast ratio below 3:1. route_color_contrast
GL116 non_ascii_or_non_printable_char warning An ID contains non-ASCII or non-printable characters. non_ascii_or_non_printable_char
GL201 foreign_key_violation error A reference points to a record that does not exist (e.g. trips.route_id, stop_times.stop_id, stops.parent_station, transfers, fare tables). foreign_key_violation
GL202 wrong_parent_location_type error parent_station references a location of the wrong type (a stop's parent must be a station, a boarding area's parent must be a stop). wrong_parent_location_type
GL203 station_with_parent_station error A station (location_type 1) has a parent_station. station_with_parent_station
GL204 location_with_unexpected_stop_time error stop_times references a station, entrance or generic node instead of a stop or boarding area. location_with_unexpected_stop_time
GL301 start_and_end_range_out_of_order error A start date/time is after its end (calendar, feed_info, frequencies). start_and_end_range_out_of_order
GL302 service_never_active warning A service_id is never active once calendar and calendar_dates are combined. -
GL303 redundant_calendar_date info A calendar_dates exception adds a date that is already active or removes one that is not. -
GL304 expired_calendar warning A service has no active dates on or after the reference date. expired_calendar
GL401 stop_times_not_sorted info The rows of a trip are not listed in increasing stop_sequence order (allowed, but slows down consumers). -
GL402 stop_time_with_arrival_before_previous_departure_time error A stop's arrival_time is earlier than the departure_time at the previous stop. stop_time_with_arrival_before_previous_departure_time
GL403 stop_time_arrival_after_departure error A stop's arrival_time is later than its own departure_time. -
GL404 stop_time_with_only_arrival_or_departure_time error Only one of arrival_time and departure_time is set. stop_time_with_only_arrival_or_departure_time
GL405 missing_trip_edge error The first or last stop of a trip has no arrival_time/departure_time. missing_trip_edge
GL406 decreasing_or_equal_stop_time_distance error shape_dist_traveled does not increase along a trip. decreasing_or_equal_stop_time_distance
GL407 stop_time_timepoint_without_times error timepoint=1 but the stop has no arrival/departure time. stop_time_timepoint_without_times
GL408 fast_travel_between_consecutive_stops warning The speed between two timed stops exceeds the plausible maximum for the route type. fast_travel_between_consecutive_stops
GL409 unusable_trip warning A trip visits fewer than two stops. unusable_trip
GL501 decreasing_shape_distance error shape_dist_traveled decreases along a shape. decreasing_shape_distance
GL502 stops_too_close warning Two different stops (not in the same station) are closer than min-stop-distance-m; often an accidental duplicate. -
GL503 stops_too_far warning Two consecutive stops of a trip are farther apart than plausible for the route type; often a wrong coordinate. -
GL504 stop_too_far_from_parent_station warning A stop, entrance or node is farther than max-parent-distance-m from its parent station. -
GL601 stop_without_stop_time warning A stop (location_type 0) is never served by stop_times. stop_without_stop_time
GL602 unused_station info A station has no child stops. unused_station
GL603 route_without_trips warning A route has no trips. -
GL604 unused_shape warning A shape is not referenced by any trip. unused_shape
GL605 unused_trip warning A trip has no stop_times. unused_trip
GL606 unused_service info A service_id from calendar/calendar_dates is not used by any trip. -
GL701 block_trips_with_overlapping_stop_times error Two trips with the same block_id run at overlapping times on a shared service date. block_trips_with_overlapping_stop_times
GL702 missing_feed_info_date warning feed_info.txt has no feed_start_date or no feed_end_date. missing_feed_info_date
GL703 feed_expiration_date7_days warning feed_end_date is less than 7 days after the reference date (or already passed). feed_expiration_date7_days
GL704 feed_expiration_date30_days warning feed_end_date is between 7 and 30 days after the reference date. feed_expiration_date30_days

Notes on rule behaviour

  • Field checks (GL101-GL116) apply to every field that the reference defines for agency, stops, routes, trips, stop_times, calendar, calendar_dates, fare_attributes, fare_rules, shapes, frequencies, transfers, pathways, levels, feed_info, translations and attributions. Newer files such as Fares v2, flex and networks are recognised, so they do not raise unknown_file, but their fields are not checked yet.
  • Required fields that depend on other fields (GL013): stop_name, stop_lat and stop_lon for location_type 0-2; parent_station for location_type 2-4; agency_id when there are several agencies; stop_id in stop_times unless location_group_id or location_id is set; the transfer stops for transfer_type 1-3 and the transfer trips for 4-5.
  • Foreign keys (GL201): trips → routes, calendars and shapes; stop_times → trips and stops; stops → parent station and levels; routes and fare_attributes → agency; frequencies → trips; transfers → stops, routes and trips; fare_rules → fare_attributes, routes and zone_id; pathways → stops; attributions → agency, routes and trips.
  • Travel speed (GL408) is checked between consecutive timed stops. Untimed stops in between add to the distance. A time difference under 60 s counts as 60 s, because most feeds round times to the minute. The default limits in km/h are: tram 100, subway 150, rail 500, bus 150, ferry 80, cable tram 30, aerial lift 50, funicular 50, trolleybus 150, monorail 150, air 1200.
  • Stop spacing (GL503) default maximum gap in km: 20 for urban rail and cable modes, 100 for bus, 500 for rail and ferry.
  • Block overlaps (GL701) are reported when two trips with the same block_id overlap in time and their services share at least one date. Frequency-based trips are skipped.
  • Expiration (GL304, GL703, GL704) is measured from --reference-date, which defaults to today. Set a fixed date in CI if builds must be reproducible.

Performance

stop_times.txt and shapes.txt are streamed one trip or one shape at a time. Small tables are kept in compact indexes, and parsed values are cached. The file is not loaded into memory. Producers almost always write the rows of a trip together. If a trip's rows are scattered through the file, gtfslint finds those trips and reads them again in a second pass, so the results stay correct.

Benchmark (benchmarks/synthetic_feed.py, a synthetic zipped feed with 100 routes, 50,000 trips, 2,000 stops and 1,000,000 stop_times rows, Python 3.12 on a Windows laptop):

Task Time
gtfslint.validate() (all rules) 6.8 s (about 150,000 rows/s)
gtfslint.stats() 6.5 s

Development

pip install -e ".[dev]"
python -m pytest --cov=gtfslint
python -m ruff check . && python -m ruff format --check src tests
python -m mypy src
python benchmarks/synthetic_feed.py /tmp/gtfs-bench

The tests use a small synthetic feed, tests/data/valid_feed, which passes every rule. tests/test_rules.py has one broken variant of it per rule.

License

MIT © Yusuf Eminoğlu. See LICENSE.

Metadata

Release files for gtfslint-cli 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gtfslint-cli 0.1.1
File Size Uploaded Provenance Actions
gtfslint_cli-0.1.1.tar.gz 68.3 kB With provenance Details for gtfslint_cli-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for gtfslint-cli 0.1.1
File Interpreter ABI Platform Actions
gtfslint_cli-0.1.1-py3-none-any.whl Python 3 none any Details for gtfslint_cli-0.1.1-py3-none-any.whl

Total release size: 118.4 kB

Release files / gtfslint_cli-0.1.1.tar.gz

Download URL gtfslint_cli-0.1.1.tar.gz
Size 68.3 kB
Tags Source
SHA-256 checksum
How to use checksums
dae1e2b0321d1151b0b02631c17318f25484d9a760e84d0ba8cbee60ca35fb4e
BLAKE2b-256 checksum
How to use checksums
3b0e95a04312c049241a7e12b983bfd9891e17b0660267925ac0c1cd209fb552
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Release files / gtfslint_cli-0.1.1-py3-none-any.whl

Download URL gtfslint_cli-0.1.1-py3-none-any.whl
Size 50.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5deba6c74efabbfc104a2b1e84bcedaae69b3fec22fb0025d5b8b6d9afe7788e
BLAKE2b-256 checksum
How to use checksums
6ec263ea1f7798a3cd676aa710d29a1e4eb93a2e457121406d25f270fb576c23
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Security for gtfslint-cli 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for gtfslint_cli-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page