# JobSpy: eight job boards behind one Python function, and the places its documentation lies

> A pandas-shaped scraper for LinkedIn, Indeed, Glassdoor, ZipRecruiter and four more boards, distributed as python-jobspy. The parameter list is genuinely useful, and it disagrees with the release notes in two specific ways.

**speedyapply/JobSpy** — Jobs scraper library for LinkedIn, Indeed, Glassdoor, ZipRecruiter & more

- Repository: https://github.com/speedyapply/JobSpy
- Stars: 4,547 · Forks: 897
- Language: Python
- License: MIT
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/speedyapply-jobspy

## One function, eight boards, one dataframe

The stated goal is aggregating all jobs from popular job boards with one tool, and the mechanism is a single function called `scrape_jobs`. The README lists what that buys you: postings pulled from LinkedIn, Indeed, Glassdoor and ZipRecruiter alongside other boards concurrently, results aggregated into a dataframe, and proxy support to bypass blocking.

Installation is a pip command, and the README states that Python 3.10 or newer is required, which matches the `requires-python = ">=3.10"` line in `pyproject.toml`.

```bash
pip install -U python-jobspy
```

Note the distribution name. The import is `jobspy`, but the package you install is `python-jobspy`, and `pyproject.toml` declares the project name as `python-jobspy` at version 1.2.0 under the MIT licence. The runtime dependencies explain most of the design: `requests` and `beautifulsoup4` for ordinary HTTP and HTML parsing, `pandas` because the return value is a dataframe, `pydantic` for the model, `curl_cffi` for TLS fingerprint impersonation, and `markdownify` to turn job descriptions into markdown.

The single-board entry point matters too. `google_search_term` is described as the only parameter for filtering Google Jobs, which means Google is not really filtered through the same query machinery as the other boards. There is more on that problem below, because it is one of the two places where this project's own documentation disagrees with itself.

## What a scrape call looks like and what comes back

The README's usage block is the whole tutorial, and it is short enough to reproduce:

```python
import csv
from jobspy import scrape_jobs

jobs = scrape_jobs(
    site_name=["indeed", "linkedin", "zip_recruiter", "glassdoor"], # "bayt", "naukri", "bdjobs"
    search_term="software engineer",
    location="San Francisco, CA",
    results_wanted=20,
    hours_old=72,
    country_indeed='USA',
)
print(f"Found {len(jobs)} jobs")
print(jobs.head())
```

The two commented lines in the original block are the ones that tell you how the library actually behaves under load. `fetch_description=True` is described as being for boards whose search results do not include the description, and as slower for that reason. `proxies` is a list of addresses in the form user:pass@host:port, with `localhost` accepted as a placeholder. Both comments are worth reading before the first real run rather than after it.

The output section of the README shows a sample dataframe with a stable column set: SITE, TITLE, COMPANY, CITY, STATE, JOB_TYPE, INTERVAL, MIN_AMOUNT, MAX_AMOUNT, JOB_URL and DESCRIPTION. That stability is the actual product. A scraper that returns a slightly different shape per board is a nuisance, and normalising salary into an interval plus a minimum and maximum is what lets you sort across boards at all.

The sample also shows a failure mode you should expect: MIN_AMOUNT and MAX_AMOUNT are `None` for both LinkedIn rows, and in one Indeed row the minimum reads 200000 against a maximum of 150000. Salary extraction from job board markup is approximate in a way that anyone writing analysis code will discover within a week.

## The parameter tree is the real documentation

Rather than prose, the README documents arguments as an ASCII tree, and that format turns out to be the most useful part of the page. The parameters that carry real weight:

- `site_name` accepts a list or a string of linkedin, zip_recruiter, indeed, glassdoor, google, bayt, bdjobs and naukri, defaulting to all of them.
- `distance` is in miles, default 50.
- `job_type` accepts fulltime, parttime, internship or contract.
- `proxies` is a list in the format user:pass@host:port, and each board scraper round-robins through them.
- `results_wanted` is per site, not in total, which quietly multiplies your request count by the number of boards.
- `offset` starts the search from a given result index.
- `hours_old` filters by hours since posting, and the README notes Glassdoor rounds up to the next day.
- `description_format` accepts markdown or html, defaulting to markdown.
- `enforce_annual_salary` converts wages to an annual figure.
- `user_agent` overrides a default that the README admits may be outdated.
- `ca_cert` takes a path to a CA certificate file for use with proxies.
- `verbose` takes 0, 1 or 2 for errors, errors plus warnings, or all logs.

Two of these deserve more attention than the tree gives them. `is_remote` sounds like a filter but v1.2.0 changed its meaning: that release notes that `is_remote` on Indeed and LinkedIn no longer guesses from the description text. If your pipeline depended on that inference, expect empties. And `easy_apply` does not filter for a boolean you supply so much as filter to jobs hosted on the board site, which is a narrower thing than the name suggests.

Country handling is per-board and inconsistent in a way that costs time. LinkedIn searches globally and uses only `location`. ZipRecruiter is limited to the US and Canada. Indeed and Glassdoor support most countries but require `country_indeed`, spelled exactly as in the table, and only the countries marked with an asterisk support Glassdoor. A separate `google_search_term` exists because Google Jobs cannot be narrowed the same way.

## The README still offers Google Jobs, and v1.2.0 says Google returns nothing

This is the first contradiction, and it is a straight one.

The parameter tree lists `google` among the valid entries for `site_name`, gives `google_search_term` its own dedicated entry with the explanation that it is the only parameter for filtering Google jobs, and `google` appears in the description of the package in `pyproject.toml` alongside LinkedIn, Indeed, Glassdoor, ZipRecruiter, Bayt, Naukri and BDJobs. The README never hedges on any of it.

Then the v1.2.0 release notes, published on 2026-10-02, state the opposite in one line: Google returns no jobs, because Google Jobs now requires JavaScript. The issue number attached is 284.

Both statements are in the project's own files on the same branch, and neither has been reconciled in the README. The most likely reading is that the scraper still accepts `google` as a site name and issues the request, but the results page no longer contains server-rendered job markup, so the parser returns an empty set rather than raising. That is a different failure from a removed feature, and it matters practically: an empty dataframe from one board looks like a no-results search rather than a broken board, so it can go unnoticed in a pipeline that only checks row counts.

The way to judge it for yourself takes a minute. Call `scrape_jobs` with `site_name="google"` alone, a `google_search_term` you know has results on the site, and `verbose=2` so the runtime printouts are on. An empty frame with no error means the README is out of date and the release note is right. Remove Google from your pipeline on that evidence, and if you need those postings, treat them as a separate problem requiring a browser automation path rather than a request library.

## Proxies, curl_cffi and the price of a description fetch

v1.2.0 is where most of the practical answers live, because it is a repair release for a scraping library being broken by other people's markup changes. Five boards are listed as returning jobs again: Glassdoor, ZipRecruiter, Bayt, Naukri and BDJobs. ZipRecruiter, Bayt, Naukri and BDJobs additionally honour more filters, including location, hours_old, job_type, is_remote, easy_apply and offset, and fill more fields.

The anti-blocking story moved under you once. `tls-client` was replaced by `curl_cffi`, which matches the `curl_cffi>=0.16.2` dependency now in `pyproject.toml`. The reason for that dependency is not explained anywhere in the repository, and it is the sort of detail that matters: a TLS client library exists to present a browser-like handshake, so if your deployment sits behind a corporate TLS-inspecting proxy or an egress filter, that handshake is the first thing likely to behave differently.

Descriptions are the expensive part. The new `fetch_description` flag replaces `linkedin_fetch_description`, and the release notes are explicit that Glassdoor descriptions now need it. The README warns of the same cost in prose: it adds one request per job, so it should be paired with proxies on larger searches. That single sentence describes the whole cost model of this library. A search of 20 results per board across 8 boards is 160 search requests; turning descriptions on turns it into roughly 320 requests, and you are now relying on the board's tolerance rather than your own patience.

Two other behaviours in the same release protect you from silent failure, and both are worth crediting. One board failing no longer discards the other boards' jobs, since boards log errors instead of raising, which means a partial result now arrives as partial data rather than as an exception. And there is one 15-second request timeout with no automatic retries, which is a deliberate choice against hammering a board that is already throttling you, at the cost of occasional missing rows.

## The repository moved to an organisation and the README images did not

The second contradiction is about identity rather than behaviour, and it is visible in the first line of the README.

The repository is `speedyapply/JobSpy`, an organisation account, and `pyproject.toml` points its Homepage at `https://github.com/speedyapply/JobSpy`. Every image in the README, including the header banner and the screenshot under the features list, points at `github.com/cullenwatson/JobSpy/assets/...`, which is the original author's personal account. Meanwhile `pyproject.toml` lists two authors, Cullen Watson and Zachary Hampton, and a separate maintainer, Kirk Watson, so the personal names survive in the package metadata.

Both facts are consistent with a project donated or transferred to an organisation while the documentation's asset URLs were never updated. Practically it means the repository you should file issues against is `speedyapply/JobSpy`, and the banner and screenshot still resolve against a different account, which is a small signal of how much of this documentation has been left alone.

The release dates are a related oddity and worth reading with care. v1.1.82 and v1.1.80 both carry GitHub release timestamps of 2026-09-30, minutes apart, and both bodies explain that the code was published to PyPI earlier and the GitHub release was added afterwards. v1.1.82 says it was published to PyPI on 2025-07-28, and v1.1.80 on 2025-03-30, with a note that version 1.1.81 was never published. So the GitHub releases page is a retrofitted changelog rather than a shipping record, and the only trustworthy install marker is the version in `pyproject.toml`.

Taken together, the picture is a small, actively maintained, single-file-scale project whose maintenance consists of chasing other companies' HTML. That is not a criticism so much as a calibration: the library is a convenience layer, and its durability depends on upstream markup staying parseable. The topics list names every board it targets individually, which is an honest signal of how tightly coupled it is to all of them.

## Conclusion

JobSpy earns its keep for a narrow, ordinary job: pull the same search across several boards at once, get a dataframe with a stable column set, and write it to CSV. Everything you need for that is in the README and the two example blocks on this page. The library is MIT licensed, the last push was on 2026-10-02, and v1.2.0 shipped the same day as a fix release rather than a feature release, which tells you the scrapers are chasing markup changes upstream rather than adding capability. Check three things before you rely on it: whether the board you care about needs `fetch_description=True`, whether Google Jobs still returns anything for your geography, and whether `is_remote` is populated from a real field or left empty. Then decide the proxy question yourself, since blocking behaviour is the part no documentation can promise.

## FAQ

### Is JobSpy a legitimate job scraper?

It is a real MIT licensed Python package, `python-jobspy`, that scrapes job boards and returns a pandas dataframe, and the README documents proxy support for getting around blocking. What the repository does not do is discuss the terms of service of the boards it targets, so whether a given use is permitted is a question about those sites rather than about this library.

### Which job boards does JobSpy support?

The `site_name` parameter accepts linkedin, zip_recruiter, indeed, glassdoor, google, bayt, bdjobs and naukri, defaulting to all of them. Coverage is uneven by design: LinkedIn searches globally, ZipRecruiter is limited to the US and Canada, and Indeed and Glassdoor require the `country_indeed` parameter with exact spellings.

### Does JobSpy still return results from Google Jobs?

Probably not, and the project contradicts itself on this. The README still lists `google` as a valid site and gives `google_search_term` its own parameter, but the v1.2.0 release notes state that Google returns no jobs because Google Jobs now requires JavaScript. An empty result from that board arrives as empty data rather than an error, so it is worth checking explicitly.

### Why is a JobSpy scrape sometimes slow?

Because descriptions are not always present in search results. The `fetch_description` flag fetches each job's page separately, which the README says adds one request per job, so a large search over several boards roughly doubles the request count and warrants proxies.

## Sources

- [Issues](https://github.com/speedyapply/JobSpy/issues)
- [License: MIT](https://github.com/speedyapply/JobSpy/blob/main/LICENSE)
- [README](https://github.com/speedyapply/JobSpy/blob/main/README.md)
- [Releases](https://github.com/speedyapply/JobSpy/releases)
- [speedyapply/JobSpy on GitHub](https://github.com/speedyapply/JobSpy)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/speedyapply-jobspy
