Open source: using it well, understanding licenses, contributing
Almost the whole stack of a machine learning project is open source: Python, NumPy, scikit-learn, pandas, pytest, MLflow, FastAPI, Git itself. Using it costs no money, but it has a different cost: every library is a promise that someone else will keep maintaining it, under a license that says what you may do with it. This lesson teaches three practical things: choosing a dependency without getting hurt, reading a license without being a lawyer, and making a first useful contribution to a project you already use. It starts from the lesson on GitHub and pull requests, whose commands it reuses.
The words of open source
Copyright assigns, by default, the rights to whoever writes a work, and nobody else may copy or modify it without permission: code you find online without a license is in this condition, so you can read it but not reuse it. A license is the written permission with which the author grants certain uses to anyone, under certain conditions, like a sign saying "free entry, no photography". Licenses fall into two families: the permissive ones (MIT, BSD, Apache) ask for little, namely that you keep the copyright notice; the copyleft ones (GPL, AGPL) ask that derived work stay free under the same conditions, a sort of inheritance of the license. It is the difference between "take the recipe" and "take the recipe, but if you publish it with changes you must publish your changes too".
A direct dependency is one you chose; the transitive ones are those your dependencies need, and in turn the ones their dependencies need, like the supply chain behind the flour you order (the mill, the grain, the transport). The maintainer is the person who looks after a project: they answer reports, review changes, publish releases, and are often a volunteer. An issue is a report (a bug, a request, a question) opened on the project's page. A vulnerability is a flaw that can be exploited to do harm, and a CVE is the public catalog number under which it is recorded, so that everyone talks about the same flaw. SPDX is the standard list of identifiers for licenses (MIT, Apache-2.0, BSD-3-Clause, GPL-3.0-only), which avoids ambiguities such as "MIT-like"; the list is at spdx.org/licenses.
The analogy: building a house with common materials
When you build a house you do not make the bricks, the steel or the pipes. You buy them from suppliers, and you trust them if they have certifications, a track record and support. An unbranded brick may last three years or five minutes. An open source library is a brick: the question is not "does it work?" (on your laptop almost all of them do) but "does it hold up over time, and may I use it for what I want?".
What "open source" means
A piece of software is open source when its code is readable and a license grants you, under certain conditions, the right to use, modify and redistribute it. The delicate point is in the conditions: it does not mean "without rules". It means the rules are written, public and identical for everyone. The list of approved licenses is kept by the Open Source Initiative.
Reading a license: the families you will meet
| License | What it asks of you | Examples in our world |
|---|---|---|
| MIT, BSD | Keep the copyright notice and the text of the license. For the rest, almost total freedom, even in closed commercial products. | pytest, FastAPI (MIT); scikit-learn, pandas, NumPy (BSD) |
| Apache 2.0 | Like MIT, plus an explicit patent grant and the obligation to flag changes made to files. | MLflow |
| GPL (copyleft) | If you distribute software that incorporates GPL code, you must distribute your own under the GPL too. It applies to distribution. | Some system tools |
| AGPL | Like the GPL, but the obligation also kicks in if you offer the software as a network service, without handing it to anyone. | Some databases and server tools |
I checked the examples column for the first two rows against the metadata of the packages installed on 2026-11-12: pytest and FastAPI declare MIT, scikit-learn and pandas BSD-3-Clause, MLflow the classifier "Apache Software License"; NumPy declares a compound expression (BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0), because it bundles components under different licenses. For the GPL and AGPL rows the examples are generic and not verified. For the first two rows the practical rule is: you can use them almost anywhere, provided you keep the notices. For the GPL and AGPL, stop and read, especially if your code will be distributed or exposed as a service to third parties. This is a guide, not legal advice: if the project is commercial and the license is doubtful, the question goes to a lawyer. choosealicense.com compares licenses in a readable way.
Model licenses are a different matter
The code and the weights of a model often have different licenses, and many model licenses are not open source licenses in the classic sense: they may limit uses (for example above certain user numbers or for certain purposes). The rule holds for any model: before building on it, open the model card, read the license and the terms of use, and check that they cover your case. The same goes for datasets: "it is public" does not mean "it is reusable".
Choosing a dependency: seven questions
Adding a dependency is a commitment. Before doing it, open the project's page and answer:
- License: is it compatible with the use I will make of it?
- Maintenance: when did the last release come out? Is there recent activity?
- People: does it have more than one maintainer, or does it depend on a single person?
- Open issues: do reports get an answer, even negative ones?
- Weight: how many dependencies does it bring along? Every transitive dependency is risk surface.
- Security: are there known vulnerabilities, and are they fixed quickly?
- Alternative: is Python's standard library enough? Ten lines of your own may cost less than a package.
The seven questions have no fixed order of importance: the license is a gate (if it is incompatible the rest does not matter), while the others are weighed together. A library with the right license but a single maintainer and no commits for two years can remain a reasonable choice for a peripheral function and a poor one for the heart of the project.
Checking the licenses in your own environment
All of the above must be checked against what you have actually installed, transitive dependencies included. Python exposes the metadata of installed packages. This script, run on 2026-11-12 in an environment created with UV (Python 3.12.13; uv add scikit-learn pandas mlflow pytest without pinning versions: scikit-learn 1.9.1, pandas 3.1.1, MLflow 3.18.0, pytest 9.2.0, NumPy 2.5.4), lists them. Sorting is by count and then by name, so the output is identical on every run.
from importlib.metadata import distributions
from collections import Counter
def license_of(meta):
# Packages declare the license in three different ways: check all of them.
expr = meta.get("License-Expression")
if expr:
return expr
for c in meta.get_all("Classifier") or []:
if c.startswith("License ::"):
return c.split("::")[-1].strip()
return (meta.get("License") or "").strip()[:40] or "NOT DECLARED"
counts = Counter(license_of(d.metadata) for d in distributions())
for lic, n in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])):
print(f"{n:3d} {lic}")
print("total", sum(counts.values()))
# Output (the first lines and the end; 19 entries in all):
# 23 MIT
# 16 BSD-3-Clause
# 13 Apache-2.0
# 11 BSD License
# 8 MIT License
# 7 Apache Software License
# ...
# 2 NOT DECLARED
# ...
# total 94
The 94 installed distributions come from four direct dependencies (uv tree --depth 1 lists exactly four): that is the surface you are accepting. The lines also show why the function is needed: the same license appears under different spellings (MIT, MIT License; BSD-3-Clause, BSD License), and a check that compares strings would treat them as distinct licenses. The trap is in the comment: a version of the script that read only the modern License-Expression field and the classifiers flagged 12 packages out of 94 as "without a license". Ten had the license written in the historical License field (for example MIT, Apache-2.0 or BSD). The packages truly without a declared license were two: the project itself (churn) and huey. A tool that reads a single field produces false alarms, and a repeated false alarm teaches you to ignore alarms. Always check that your control has no blind spot before trusting its outcome. The module that exposes the metadata is described in importlib.metadata.
The first contribution: small, useful, acceptable
Contributing does not mean rewriting a module. The best entry point is a small, verifiable fix in a project you already use, because you understand its problem: a typo or a wrong example in the documentation, an unclear error message that cost you half an hour, a missing test for a case you found. Even a well-made report (an issue) is a contribution, with the version, steps to reproduce, expected behavior and actual behavior. The Open Source Guides list the same forms of contribution.
The procedure, with the commands of the previous lesson, has an order worth respecting:
- Read
CONTRIBUTING.mdand the project's code of conduct: they explain how the project wants to receive contributions. - Check whether an issue or a PR on the same subject already exists.
- Fork, create a branch, change one thing only, add or update the test.
- Open the PR explaining the problem, the fix and how you verified it.
- Accept the review patiently: maintainers are often volunteers, and a request for changes is not a rejection.
The first two steps avoid wasted work: a PR that ignores the project's rules or duplicates one already open is sent back without being read. The third is the rule of the minimal change, which makes review possible for a volunteer with half an hour to spare.
Traps and trade-offs
The first trap is believing that "it is on GitHub, so it is free": a repository without a license file grants you no right of reuse. The second concerns pinned versions: they protect against sudden changes (as you see in UV, environments and dependencies) but make you miss security fixes if you never update, so you need an update routine. Copying code from an answer online does not free it from the license constraints that code has. And more dependencies speed you up today and slow you down tomorrow: every package is a supplier to keep an eye on.
How well does the dependency stand? Looking at it with commands
The seven questions have answers that you read, not guess. For maintenance, the project's page on PyPI (the Python package index) shows the date of the latest release, and from a clone git log -1 --format=%cd prints the date of the last commit: a project idle for two years is not automatically dead (it may be "finished"), but you should know. For weight, uv tree prints the tree of all dependencies, direct and transitive. For security there are tools that compare the versions you have with public vulnerability databases; I ran pip-audit 2.10.1 (a project supported by the Python Packaging Authority) on the lock exported from the test project:
uv export --no-dev --format requirements-txt --no-hashes -o requirements.txt
uvx pip-audit -r requirements.txt --no-deps --disable-pip
# No known vulnerabilities found
# Name Skip Reason
# ---- -----------------------------------------------------
# -e - could not deduce package version from URL requirement
The first line turns the lockfile into a "package==version" list (uv export); uvx runs the tool in a temporary environment without installing it in the project; --no-deps tells it not to resolve further dependencies (the list is already complete and pinned) and --disable-pip not to use pip to do so. The tool also prints two warnings saying it would be better to have hashes, because here I removed them with --no-hashes. A single entry was skipped, the project itself (-e), which has no published version to compare. The meaning is narrow: "no known vulnerability at that date". A clean result today is not clean tomorrow, so the check must be rerun periodically.
The license of your own project
Your own repository, if you make it public, also needs a license: without one, nobody can legally reuse it, however online it is. For code you want to let people reuse freely, the most common choice is MIT or Apache-2.0; if you want those who improve it to have to give the improvements back, the GPL. The practical procedure is always the same: choose the license (choosealicense.com helps with simple questions), put the text in a LICENSE file at the root and declare it in the metadata, in pyproject.toml with license = "MIT" using the SPDX identifier, so that tools, including the script above, read it without ambiguity.
Model and data licenses: one more check
What holds for code holds, in addition, for two objects typical of machine learning. The model weights are often published under a license of their own, written specifically for models, which may limit uses (for example forbidding certain purposes, or imposing conditions above a certain scale of use); it is not certain that it is "open source" in the sense of the official definition, so check the model card. Data has licenses in turn: the Creative Commons families are the most widespread, and CC BY asks for attribution, CC BY-NC forbids commercial use, CC0 waives the rights. On top of that, personal data carries the additional constraint of privacy law, independent of the license: "downloadable" does not mean "reusable for any purpose".
The safest way for an ML project is a small file (even just a README or a table) that lists, for each model and dataset, name, version, license and source. It costs ten minutes and answers the first question of any customer or reviewer.
A first contribution, step by step
Suppose you have found an error in the documentation of a library you use: an example does not work. Here is the complete path with the commands from the GitHub lesson (made-up repository names). The blocks that follow are a scheme to adapt and were not run, because they require a GitHub account and a real repository.
gh repo fork organization/library --clone # fork on GitHub + local clone
cd library
git remote -v # origin = your fork, upstream = the original
git switch -c docs/fix-quickstart-example
gh repo fork ... --clone (GitHub CLI) creates your copy on your account and clones it, also setting upstream. Then a branch with a name that says what you are doing.
# ... fix the documentation file ...
git diff # reread what you changed
git commit -am "docs: fix the quickstart example"
git push -u origin docs/fix-quickstart-example
gh pr create --fill
The diff before the commit is your personal review. gh pr create --fill opens the pull request filling in title and description from the commit; then edit the description following the template from the GitHub lesson: what, why, how you verified it. In the case of a wrong example, the verification is "I ran the corrected example and it produces the output the documentation states".
What to expect afterwards: a maintainer may reply in hours or in weeks; they may ask you to change something (answer with new commits on the same branch); they may close the PR explaining that the direction does not match the project. Nothing personal: in all three cases you learned something and, in the first, you improved a tool you use every day.
Reporting an error well (issue)
Even a well-made report is a contribution, and often the most valuable one. A good issue always has the same parts:
Title: search_runs ignores order_by when the metric name contains a dot
Versions: mlflow 3.18.0, python 3.12.x, sqlite backend
Steps to reproduce:
1. ...
2. ...
Expected: runs sorted by auc, descending
Actual: insertion order
Notes: I tried with ... and the behavior stays the same
The text is purely illustrative of the format: it does not describe a real MLflow error. The versions and a minimal, repeatable procedure are what turn "it doesn't work" into something a maintainer can work on.
The why: you cannot build everything, and what you do not build is a risk you accept
A modest machine learning project imports few direct libraries and, as seen with the count above, almost a hundred packages in all. Nobody wrote them alone and nobody could: the value of free software is precisely that the work of a great many people is available for free. But "free" does not mean "without costs": it means the costs have moved. You no longer pay for a license; you pay in attention. You must know what you are importing, under which conditions, and what you will do when a dependency changes, stops being maintained or has a flaw.
The term that describes the set of these risks is the software supply chain. The metaphor is that of an industrial food chain: your end product depends on suppliers who depend on other suppliers, and a problem upstream reaches downstream without you having seen it. The typical risks fall into four families, and it is worth telling them apart because they have different defenses. Abandonment is time passing without any attack: the project is no longer maintained, vulnerabilities are not fixed and the library stops working with new versions of Python; the defense is to choose with the seven questions and to know how hard it would be to replace.
The flaw is a security or logic error, and the defenses are audit tools (pip-audit), regular updates and tests that catch behavior changes. The change of terms is the license that changes or the project that moves to a commercial model: you read the license of the version you use and know in advance what you would do. Tampering is someone publishing a malicious package, sometimes with a name almost identical to a well-known one (typosquatting, the same idea as the scam with a website whose address resembles the bank's), or compromising a legitimate package; against it, lockfiles with hashes prevent the silent substitution of a file already pinned, and you need to install only what you decided on and check the spelling of names when you add a dependency.
No paranoia is needed: you need to know that every uv add is a decision and not an automatic gesture.
Adopt, wrap, avoid: three answers to a dependency
Faced with a candidate library there is not just "use it or not". There are three attitudes, in increasing order of caution.
Adopt. The library is widespread, maintained, has a compatible license and its role is central (think of NumPy or scikit-learn). You use it directly, with no detours, because the extra abstraction would cost more than the risk. The cost of replacing it would be enormous anyway (it is the whole project), so you choose it carefully once and then keep it updated.
Wrap. The library is needed but less established, or it is a detail you might want to change (a client for an external service, a parser for a niche format). You write a small layer of your own (a function or a class) that uses it, and the rest of the code talks only to that layer. If one day you must replace it, you change one file. The cost is one more function to maintain and test, worth paying only where replacement is genuinely plausible.
Avoid. If for a ten-line function you add a dependency with dozens of transitive packages, you have made a bad deal. Sometimes Python's standard library is enough, sometimes it is better to write the function. The practical rule: the smaller the dependency compared to what it brings with it, the less you want it.
An example of project-level policy: what this site chooses not to use
Decisions about dependencies concern not only libraries but also services. In the project's rules (the CLAUDE.md file at the root of the repository) there is an explicit list of rejected external services: the GitHub API, S3, Plausible, Sentry, with the instruction to use self-managed or free alternatives (line checked on 2026-11-12). Whatever you think of the choice, it is a good model of method: the decision is written down, justified in one line, and applies to all future contributors. Whoever comes later does not have to guess, and any exception must go through discussion instead of happening by distraction.
The project also makes a useful distinction: naming a product as a subject (an article comparing tools, a tool that declares compatibility with various assistants) is fine; declaring it as your own supplier when it is not would be false. It is the same distinction that, in an ML project, separates "the paper we compare against" from "the component we use": real dependencies and cited ones must be kept distinct in every document, model card included.
Compatibility between licenses: when you combine code from different sources
The problem arises when your project combines components under different licenses. Two basic rules, to be read as orientation and not as legal opinion.
A permissive license (MIT, BSD, Apache 2.0) can be included in a project under any license, even proprietary or copyleft, provided its obligations are respected (keeping the notice). That is why the foundational libraries of the ML ecosystem are permissive: they can be used anywhere.
A strong copyleft license (GPL) requires that derived work, if distributed, be released in turn under the GPL. So incorporating GPL code into a distributed proprietary product is a problem. The keyword is "distributed": if you use a GPL program internally, without giving copies to anyone, the obligation generally does not kick in. The AGPL closes precisely this loophole: it counts offering the software as a network service as "distribution" too. For an ML service exposed over the network that embeds an AGPL library, the obligation may kick in. This is the case where it is worth stopping and asking.
There are also projects under a "source-available" license (the code is readable but the terms are not those of the open source definition: for example, they forbid use as a competing service). They are legitimate, but they are not open source in the sense of this lesson, and must be read with the same care as models under their own license. The SPDX identifier in the metadata or in the LICENSE file is the first place to look.
Contributing: the certificate of origin and the house rules
Many projects ask, together with your change, for a statement that you have the right to contribute it. Two common forms. The CLA (contributor license agreement) is a formal agreement signed once. The DCO (developer certificate of origin, text at developercertificate.org) is lighter: a Signed-off-by: Name <email> line in every commit, with which you declare you have the rights. Git adds it by itself with the -s option:
git commit -s -m "docs: sign-off test"
git log -1 --format=%B
# docs: sign-off test
#
# Signed-off-by: Test <a@b.c>
The -s (--signoff) added at the bottom of the message the line with the name and email configured in Git (how to set them, you see in Git, the time machine of your project); I ran it in a test repository with Git 2.53. Always read the project's rules before contributing: the CONTRIBUTING.md and CODE_OF_CONDUCT.md files say how they want contributions, in which language, with what style and with which tests, and a PR that ignores them is sent back without being read.
Opening your own project: the minimum for it to be truly reusable
If one day you publish a project of your own (a model, a tool, a dataset), a few files make the difference between "a public folder" and "a project that others can use". This site's repository has a template for its own public projects, in the scripts/templates/oss-repo/ folder: a README to fill in, a CITATION.template.cff (the file that allows the software or dataset to be cited correctly), a script that checks for secrets before publishing (secret-scan.sh) and a checklist (the folder contains these files, verified with ls). The essential elements, in order of importance:
- README: what the project does in two sentences, how to install it and run an example in three commands, how to contribute. Whoever does not understand the project in thirty seconds will not try it.
- LICENSE: without it, the project is not reusable.
- A way to reproduce:
pyproject.tomlanduv.lock, so that "install and run" works. - Tests (pytest) and, if you have a CI, a green badge at the top of the README.
- No secrets in the history: before publishing, scan the whole Git history, not just the current files, because a deleted secret remains in the old commits.
- A card for each model or dataset (model card, dataset card): where the data comes from, under which license, what limits and discouraged uses.
These items are, at bottom, the same questions as this lesson seen from the side of whoever publishes: make it easy for someone else to answer the seven questions about your project. The order matters: without a README nobody gets to the rest, without a LICENSE nobody can use what they find.
Giving something back: beyond code
Not all contributions are code, and often the most useful ones are not: correct documentation, examples that work, translations, answers to people with the same problems, reproducible reports, and simply thanking a maintainer. If your company or your project depends on a library, supporting it financially (many projects have ways to do so) or dedicating paid hours to it is the most direct way to reduce the risk of abandonment, the first on the list. A system that depends on volunteer work without giving anything in return is, at bottom, taking advantage of an insurance that someone else pays for.
Try it yourself: four guided exercises
Exercise 1: audit of your environment's licenses
In a UV project with at least a few dependencies (for example uv add scikit-learn pandas), save the licenses script in licenses.py and run it with uv run python licenses.py. Compare it with the number of packages: uv run python -c "from importlib.metadata import distributions; print(len(list(distributions())))". What to check: in my project, 94 packages (your number will depend on the versions resolved that day), with licenses almost all permissive (MIT, BSD, Apache) and a single Mozilla Public License 2.0. Look for the ones your script flags "NOT DECLARED" and check, by opening the package's page on PyPI, whether the license really exists. What you learn: that metadata is heterogeneous and that a checking tool must be tried on edge cases.
Exercise 2: the security audit
Export the lock and check it: uv export --no-dev --format requirements-txt --no-hashes -o requirements.txt, then uvx pip-audit -r requirements.txt --no-deps --disable-pip. You should see a summary saying whether known vulnerabilities exist for the pinned versions (in my test, No known vulnerabilities found). Repeat after a few weeks, or on an old lock, and compare. What you learn: that "clean today" is not "clean tomorrow".
Exercise 3: evaluate a dependency with the seven questions
Pick a library you use that is not a load-bearing pillar (not NumPy: a "peripheral" library). Fill in a table with the seven questions of the lesson using its PyPI page and its repository. For the "weight" use uv tree --package name-of-the-library or compare uv tree before and after adding it. What you learn: to decide with data instead of habit, and to discover that some dependencies cost much more than they seem. Note down the decision (adopt, wrap, avoid) with a one-line reason.
Exercise 4: the sign-off and a first report
In a test repository, make a commit with git commit -s and read the message with git log -1 --format=%B: you should see at the bottom the line Signed-off-by: Name <email> with the data configured in Git. Then look, in a library you use, for an open issue that concerns you or an unclear sentence in the documentation, and write (even just in a text file) the report in the lesson's format: title, versions, steps, expected and actual. What you learn: that contribution work starts from a precise description, and that almost always what looks like "my problem" is useful information for the maintainer.
Exercises 1 and 2 take a few minutes and have immediate practical value for any project of yours: knowing what is inside your environment is the first step to trusting it.
What you can do now
You can say that open source means written rules, not the absence of rules, and tell the permissive licenses (MIT, BSD, Apache 2.0) from those that impose obligations on your code (GPL, AGPL). You know that models and datasets have licenses of their own to read before building on them, and that before adding a dependency you ask seven questions. You can check the licenses and the vulnerabilities of your environment, and you know the check must be tried on edge cases, because the first licenses script of this lesson had a blind spot. Finally you know how a first contribution begins: with a small, verified fix. Next lesson: UV, environments and dependencies that repeat identically.
The same series also includes ML-Ops: multi-stage Dockerfile for an ML service, non-root user and healthcheck and ML-Ops: container registries, tags, digests and versions that do not change under your feet.
Sources
- Open Source Initiative: licenses - the list of licenses approved as open source, consulted on 2026-11-12
- choosealicense.com - readable comparison between licenses (permissive, copyleft, Apache 2.0 patents), consulted on 2026-11-12
- SPDX License List - the license identifiers, consulted on 2026-11-12
- importlib.metadata, Python documentation - reading of package metadata used by the script, consulted on 2026-11-12
- pip-audit on PyPI - the audit tool used, version 2.10.1, consulted on 2026-11-12
- Open Source Guides: how to contribute - the forms of contribution and the procedure, consulted on 2026-11-12
- Developer Certificate of Origin - the DCO text that the Signed-off-by line refers to, consulted on 2026-11-12
The licenses script was run twice (Python 3.12.13, UV 0.12.21, environment created on 2026-11-12) with identical output; pip-audit and git commit -s were run once with the outputs shown. The gh commands and the fork were not run, because they require a GitHub account. The number 94 and the versions change with every resolution of the dependencies. The line on rejected services comes from the CLAUDE.md file of the site's repository (grep of 2026-11-12). The MLflow issue title in the template is invented.
Commenti
Caricamento commenti...
Sign in to comment