Is This Library Worth Installing?
A five-minute check before you take on a dependency you will keep for years
In this lesson
pip install is four seconds of typing and a commitment measured in years. Every package you add is code you did not write, cannot see, and will have to keep working — and the decision is usually made in about two seconds, from the first search result.
This lesson is the check to run instead. It takes five minutes, and it is most of the difference between a project that still installs in three years and one that does not.
Explain it like I’m 5
Installing a library is adopting a pet. It is free to take home and it needs feeding for years, so it is worth a look before you say yes.
Every dependency is a small permanent commitment
A dependency is not just a feature you gained. It is:
- Code that has to keep working when Python releases a new version.
- A thing your requirements file has to pin, and someone has to bump.
- A surface where a security advisory can arrive.
- Something a future reader has to learn to understand your project.
None of that is an argument against dependencies. Writing your own HTTP client instead of using requests would be a much worse idea. It is an argument for spending five minutes on the ones you add.
Ask the standard library first
The most common unnecessary dependency is one that replaces something Python already ships. Python's standard library is unusually large, and a surprising amount of what people install is already installed.
Things you already have, with no pip install at all:
- Files and paths —
pathlib,shutil,glob,tempfile,zipfile. - Data formats —
json,csv,tomllib,sqlite3,configparser. - Command-line tools —
argparse,logging,os.environ. - Structure and iteration —
dataclasses,collections,itertools,functools,enum. - Dates, numbers, randomness —
datetime,zoneinfo,decimal,statistics,random,secrets. - Testing and checking —
unittest,doctest,hashlib,difflib.
So the first question is never “which package does this?” It is “does Python already do this?” When the answer is yes and the package is a slightly nicer interface over it, the package is usually not worth the commitment.
The five-minute check
Five things, all of them visible on the package's PyPI page and its repository in a couple of minutes.
- When was the last release? Under a year is healthy. Over two years, assume nobody is coming when it breaks.
- How many people maintain it? One is a risk, not a disqualification — it depends how much code you would have to replace.
- Do the docs have runnable examples? No examples means you will be reading the source, which is time you did not budget.
- How many packages does it drag in? See the next section.
- Is there a license? No license means no permission. See below.
PACKAGES = [
dict(name="requests", months_since_release=2, maintainers=12,
runnable_examples=True, dependencies=4, license="Apache-2.0"),
dict(name="fastcsvthing", months_since_release=41, maintainers=1,
runnable_examples=False, dependencies=17, license=""),
dict(name="tinyslug", months_since_release=5, maintainers=1,
runnable_examples=True, dependencies=0, license="MIT"),
]
def concerns(package):
"""Everything about a package that should give you pause."""
found = []
if not package["license"]:
found.append("no license at all: you have no permission to use it")
if package["months_since_release"] > 24:
found.append(f"nothing released for {package['months_since_release']} months")
if package["maintainers"] == 1:
found.append("one maintainer: everything stops if they stop")
if not package["runnable_examples"]:
found.append("documentation with no runnable example")
if package["dependencies"] > 10:
found.append(f"installs {package['dependencies']} other packages with it")
return found
for package in PACKAGES:
problems = concerns(package)
print(f"{package['name']:13s} {len(problems)} concern(s)")
for problem in problems:
print(" -", problem)
requests 0 concern(s)
fastcsvthing 5 concern(s)
- no license at all: you have no permission to use it
- nothing released for 41 months
- one maintainer: everything stops if they stop
- documentation with no runnable example
- installs 17 other packages with it
tinyslug 1 concern(s)
- one maintainer: everything stops if they stop
How many packages am I really installing?
A dependency has dependencies. Asking for one package regularly installs five or twenty, and every one of them is code you now ship. pip show tells you the direct ones for anything installed:
$ pip show requests
Name: requests
Version: 2.34.2
Summary: Python HTTP for Humans.
License: Apache-2.0
Requires: certifi, charset_normalizer, idna, urllib3
Required-by: msal, wagtail
$ pip show certifi charset_normalizer idna urllib3 | grep -E "^(Name|Requires)"
Name: certifi
Requires:
Name: charset-normalizer
Requires:
Name: idna
Requires:
Name: urllib3
Requires:
Work out what a package really costs. everything_installed walks the dependency graph and returns every package that arrives alongside the one you asked for; uninvited then reports how many of those you never mentioned. Handle a package with no dependencies too — the graph should not care.
DEPENDS_ON = {
"reportmaker": ["pandas", "jinja2", "click"],
"pandas": ["numpy", "python-dateutil", "pytz"],
"jinja2": ["markupsafe"],
"python-dateutil": ["six"],
"click": [], "numpy": [], "pytz": [], "markupsafe": [], "six": [],
}
def everything_installed(package, graph):
"""Every package that arrives alongside `package`, sorted."""
found = set()
queue = list(graph.get(package, []))
# TODO: while the queue is not empty, take a name off it; if it is new,
# record it and add ITS dependencies to the queue
return sorted(found)
def uninvited(package, graph):
"""How many arrive that you never asked for."""
return 0 # TODO: everything installed, minus what you actually named
print("you asked for:", sorted(DEPENDS_ON["reportmaker"]))
print("you installed:", everything_installed("reportmaker", DEPENDS_ON))
print("uninvited: ", uninvited("reportmaker", DEPENDS_ON))
print("a leaf package:", everything_installed("six", DEPENDS_ON),
uninvited("six", DEPENDS_ON))
The found set is doing two jobs: collecting the answer and remembering what you have already queued, which is what stops a circular dependency looping forever. Use graph.get(name, []) rather than graph[name] so an unknown package is treated as having no dependencies instead of raising.
DEPENDS_ON = {
"reportmaker": ["pandas", "jinja2", "click"],
"pandas": ["numpy", "python-dateutil", "pytz"],
"jinja2": ["markupsafe"],
"python-dateutil": ["six"],
"click": [], "numpy": [], "pytz": [], "markupsafe": [], "six": [],
}
def everything_installed(package, graph):
"""Every package that arrives alongside `package`, sorted."""
found = set()
queue = list(graph.get(package, []))
while queue:
name = queue.pop()
if name not in found:
found.add(name)
queue.extend(graph.get(name, []))
return sorted(found)
def uninvited(package, graph):
"""How many arrive that you never asked for."""
return len(everything_installed(package, graph)) - len(graph.get(package, []))
print("you asked for:", sorted(DEPENDS_ON["reportmaker"]))
print("you installed:", everything_installed("reportmaker", DEPENDS_ON))
print("uninvited: ", uninvited("reportmaker", DEPENDS_ON))
print("a leaf package:", everything_installed("six", DEPENDS_ON),
uninvited("six", DEPENDS_ON))
you asked for: ['click', 'jinja2', 'pandas']
you installed: ['click', 'jinja2', 'markupsafe', 'numpy', 'pandas', 'python-dateutil', 'pytz', 'six']
uninvited: 5
a leaf package: [] 0
Read the license line
The license decides what you are allowed to do with the code. It takes ten seconds to check and it is the one item on the list with consequences you cannot fix later by swapping the package out.
- MIT, BSD, Apache-2.0 — permissive. Use it in anything, including commercial work; keep the license notice. This is most of the Python ecosystem.
- GPL, AGPL — copyleft. Fine for your own tools and for open projects, but there are real conditions if you distribute or host software built on them. Read before shipping commercially.
- No license at all — the important case. Code published with no license is not public domain; by default nobody has permission to use it. “It was on GitHub” is not permission.
That is a summary, not legal advice, and anything with money attached deserves a real check. But the habit is simple: look for the license, and treat its absence as a red flag rather than a formality.
Learning it properly once you have chosen
Having picked a library, the fastest route in is almost always the same order:
- Its own getting-started page, run rather than read, using the technique from the previous lesson: run the example, change one thing, predict the result.
- Its API reference, once you have a question. Not before.
- One book or one course, if the subject is big enough to deserve it — a web framework or pandas, say. One. A second course on the same subject is procrastination with a receipt.
- Its source and its issue tracker, when behavior surprises you. A search of closed issues answers “is this a bug or is it me?” faster than anything else.
Judging a course or a book is the same problem as judging a package: is it current (check the Python version it uses), does it build something (a course with no project produces no skill), and can you see a sample (a free chapter or a preview lesson). Free official documentation beats a paid course that is two major versions out of date.
Common mistake: Installing the first search result
It appeared to solve the problem, and installing is faster than checking.
Spend the five minutes: last release, maintainers, examples, dependency count, license. Search ranking measures popularity of the page, not health of the project.
Common mistake: Adding a whole package for one small function
Somebody has already written it, so writing it yourself feels wasteful.
If the function is ten lines and you understand it, write the ten lines. If it is date-and-timezone handling or anything cryptographic, take the package — those are the ones where subtle mistakes are expensive.
Common mistake: Never checking the license
It feels like paperwork, and public code feels free by default.
Look at one line on the PyPI page. Permissive licenses cover most of what you will meet; no license at all is the case worth catching, because it means nobody granted permission.
Common mistake: Judging a package by its GitHub stars
Stars are the most visible number on the page.
Stars accumulate and never decay, so an abandoned project keeps them all. Look at the date of the last release and the last few closed issues instead.
Common mistake: Never pinning versions, or upgrading everything at once
Both are the path of least effort: install whatever is current, then one day run a bulk upgrade.
Pin what you depend on in your requirements file, then upgrade one package at a time with your tests running. Ten upgrades in one commit produce one failure and ten suspects.
What should you ask before choosing between two packages?
A package that duplicates pathlib, json, csv, or dataclasses is a commitment with no gain. Ask what Python already ships first.
A package you need has exactly one maintainer. What does that mean?
Two hundred lines you could rewrite in an afternoon is a risk you can carry. Something you could never reimplement is a different conversation.
Why count a package's dependencies before installing it?
Asking for one package can install twenty. Each one is code in your project, a line in your lockfile, and a place an advisory can land.
A useful-looking repository has no license file. What can you do with it?
Copyright applies by default, so “no license” means “all rights reserved”. The polite move is to open an issue asking the author to add one.
Mini exercise (medium)
Turn the five-minute check into a single verdict. verdict(package) returns one of "avoid", "not needed", "with care" or "use it", testing the rules in that order: no license or nothing released for over two years is "avoid"; a standard-library alternative is "not needed"; a single maintainer is "with care"; anything else is "use it".
Try it yourself. Complete the snippet and hit Run; it executes in your browser, no setup required.
CANDIDATES = [
dict(name="httpx", months=1, maintainers=9, license="BSD-3-Clause",
dependencies=5, stdlib_alternative=""),
dict(name="pathtools", months=8, maintainers=1, license="MIT",
dependencies=0, stdlib_alternative="pathlib"),
dict(name="fastjson", months=3, maintainers=1, license="",
dependencies=2, stdlib_alternative="json"),
dict(name="oldretry", months=52, maintainers=3, license="MIT",
dependencies=1, stdlib_alternative=""),
dict(name="tinyretry", months=7, maintainers=1, license="MIT",
dependencies=0, stdlib_alternative=""),
]
def verdict(package):
"""One of: "avoid", "not needed", "with care", "use it"."""
return "?" # TODO, in this order:
# no license -> "avoid"
# nothing for 2 years -> "avoid"
# the stdlib does it -> "not needed"
# one maintainer -> "with care"
# otherwise -> "use it"
for package in CANDIDATES:
print(f"{package['name']:10s} {verdict(package)}")
Five if statements, each returning immediately, then a final return "use it". Order matters: an abandoned package with a stdlib alternative should come back as "avoid" rather than "not needed", because the first check reached is the one that answers.
CANDIDATES = [
dict(name="httpx", months=1, maintainers=9, license="BSD-3-Clause",
dependencies=5, stdlib_alternative=""),
dict(name="pathtools", months=8, maintainers=1, license="MIT",
dependencies=0, stdlib_alternative="pathlib"),
dict(name="fastjson", months=3, maintainers=1, license="",
dependencies=2, stdlib_alternative="json"),
dict(name="oldretry", months=52, maintainers=3, license="MIT",
dependencies=1, stdlib_alternative=""),
dict(name="tinyretry", months=7, maintainers=1, license="MIT",
dependencies=0, stdlib_alternative=""),
]
def verdict(package):
"""One of: "avoid", "not needed", "with care", "use it"."""
if not package["license"]:
return "avoid"
if package["months"] > 24:
return "avoid"
if package["stdlib_alternative"]:
return "not needed"
if package["maintainers"] == 1:
return "with care"
return "use it"
for package in CANDIDATES:
print(f"{package['name']:10s} {verdict(package)}")
httpx use it
pathtools not needed
fastjson avoid
oldretry avoid
tinyretry with care
assert verdict(CANDIDATES[0]) == "use it", "recent, several maintainers, licensed, and nothing in the stdlib does it"
assert verdict(CANDIDATES[1]) == "not needed", "pathlib already does this, so the package is not the question"
assert verdict(CANDIDATES[2]) == "avoid", "no license is the first thing that rules a package out"
assert verdict(CANDIDATES[3]) == "avoid", "52 months with no release is abandoned, however many maintainers it lists"
assert verdict(CANDIDATES[4]) == "with care", "healthy, but a bus factor of one"
assert verdict(dict(name="x", months=30, maintainers=4, license="", dependencies=0, stdlib_alternative="")) == "avoid", "unlicensed AND stale still comes back as one verdict"
assert verdict(dict(name="x", months=30, maintainers=4, license="MIT", dependencies=0, stdlib_alternative="json")) == "avoid", "check the release date before the stdlib question: an abandoned package is a dead end either way"
print("\u2713 Looks good!")