Sitemap
MITB For All

Tech contents related to AI, Analytics, Fintech, and Digital Transformation. Written by MITB Alumni; open-access for everyone.

Building Python wheel for third-party execution

Step-by-step guide that can be replicated in minutes

8 min readSep 12, 2025

--

In Python, a wheel is a built distribution format. A .whl file contains the files and information needed for installing a Python package (though pip resolves and installs dependencies). Whether you know it or not, it is almost certain that you had installed packages from wheels fetched from PyPI.

A library (eg. numpy) is a collection of one or more packages, while a package is a collection of modules (think .py files, but not exclusively just .py) grouped together into a namespace for functionality.

Whenever we pip install, we are downloading other people’s code.

Therefore, whether or not you need to distribute wheels, it is cool to see what actually goes on behind the scenes!

Press enter or click to view image in full size
Image created by DALL·E 3 based on the author’s prompt.

Background

Recently, I created a project for CS609 (a Master’s level course on Reinforcement Learning). The objective is for students to obtain observations from an environment, such that given a current state and action it returns the next_state.

I packaged the underlying script into a wheel, to be distributed for execution without explicitly revealing the source code. I’ve also obfuscated the source code. Of course, with enough determination (and time), everything can be reverse-engineered. But there wouldn’t be a tangible advantage for such a hacker/cheater, because students are required to use RL algorithms and not rule-based methods.

And why do I not host it on the cloud, for example using AWS Lambda and API Gateway? I want zero risk of any form of attacks (eg. extremely large number of requests 24/7 from a bot, racking up large bills). Setting a throttling limit is not a perfect solution, as it may result in legitimate users being blocked from access while the attack is happening. Of course I could then go into API keys, and even deeper, but remember that this is just part of a project from one course, which is turn is just one of my many tasks.

I have always tended towards the “data science” side of things rather than the “software engineering” side of things. To me, the math, algorithms and theories are way more beautiful than executing command lines in a prescribed fashion, and I rather spend my time learning and doing data science stuff.

Therefore, a straightforward and quick approach described in this article is good enough. It serves its purpose, that is to provide the RL environment for training.

It is really like using a simply bicycle lock. It deters passers-by who have no intention to steal your bicycle, but wouldn’t be enough to stop a determined thief. In fact, the same can be said for a regular house lock.

Most likely, this ‘good enough’ applies to you to.

For those who are curious about the steps, here’s how I did it.

Set-up

Script

The environment transition formulas are defined in my <_internal_math.py> script, under the following directory structure.

project/
└── oceanrl/
├── __init__.py
├── _internal_math.py
└── api.py
def update_populations(salmon_t, shark_t, fishing_effort_t, month_t):
"""
Update salmon and shark populations for one time step (month)
"""
### Fun stuff inside.
return salmon_caught_t, salmon_t_plus_1, shark_t_plus_1

After using the replace-all function in a code editor, the contents looks as shown below.

<__init__.py> just contains from .api import query, while <api.py> is the public-facing side of things.

from . import _internal_math as _

def query(salmon_t, shark_t, fishing_effort_t, month_t):
return _.update_populations(salmon_t, shark_t, fishing_effort_t, month_t)

def main():
import argparse, json
p = argparse.ArgumentParser()
p.add_argument("--salmon", type=float, required=True)
p.add_argument("--shark", type=float, required=True)
p.add_argument("--effort", type=float, required=True)
p.add_argument("--month", type=int, required=True)
a = p.parse_args()
out = query(a.salmon, a.shark, a.effort, a.month)
print(json.dumps({
"salmon_caught": float(out[0]),
"salmon_t_plus_1": float(out[1]),
"shark_t_plus_1": float(out[2]),
}))

Next, we will also add pyproject.toml in the same level as ocreanrl/ folder (not within).

project/
├── pyproject.toml
└── oceanrl/
├── __init__.py
├── _internal_math.py
└── api.py

With the appropriate values plugged into the boilerplate, we have pyproject.toml as follows.

[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "oceanrl"
version = "0.1.0"
description = "Protected simulation wrapper"
readme = "README.md"
requires-python = ">=3.12,<3.13"
authors = [{ name = "JK" }]

[tool.setuptools.packages.find]
where = ["."]
include = ["oceanrl*"]

[tool.setuptools.package-data]
oceanrl = ["*.pyc"]

[project.scripts]
oceanrl-query = "oceanrl.api:main"

Bytecode

In your command terminal, cd into the project\ folder (not inside oceanrl\) and compile to a hash-based .pyc by executing:

python -m compileall --invalidation-mode=checked-hash -b oceanrl/_internal_math.py

Almost instantaneously, you will see Compiling 'oceanrl/_internal_math.py'... on your terminal. If you go into your file explorer, you will also see _internal_math.pyc. You may now delete _internal_math.py (ie. the python script) and retain just the .pyc file.

Contents that you expect to see within [coeanrl/], before deleting anything.

Build the wheel

At this point, you should have the following:

project/
├── pyproject.toml
└── oceanrl/
├── __init__.py
├── _internal_math.pyc
└── api.py

With this, you are now ready to build the wheel! In your command terminal where you have left off previously (at project\), run

python -m build --wheel --no-isolation -o dist

You will see * Getting build dependencies for wheel... at the beginning, and after a couple of seconds you will the something similar to below once it is completed.

Press enter or click to view image in full size

Check back on your file explorer and you will see three new folders — build\, dist\ and oceanrl.egg-info. You only need to distribute oceanrl-0.1.0-py3-none-any.whl within the dist\ folder.

Adding Libraries

If you require libraries that are not available in the default python, they have to be declared as dependencies within [project] of the .toml file. When the user pip installs your package (not shown yet; see the next section on ‘Using the wheel’), those libraries will be automatically downloaded (assuming there is internet connection). You can also download wheels from PyPI and distribute them so that everything can be installed offline.

Using the wheel

Let’s run everything on the cloud to test things out. I will use Google, since it is free and used by my target audience.

Google Drive

First, place the oceanrl-0.1.0-py3-none-any.whl file in Google Drive (https://drive.google.com/drive/my-drive). Here, I created a folder named wheels to hold the .whl file.

Press enter or click to view image in full size
Screenshot of my personal Google Drive.

Google Colab

We will mount our drive on the Colab notebook so that the contents can be accessed.

from google.colab import drive
drive.mount('/content/drive')

Next, we will install the wheel so that the query function can be imported from oceanrl. You can either use the terminal on Colab (click on the bottom left), or add ! in the regular notebook cells. Here, I assume you are using the latter.

!ls drive/MyDrive/wheels

Check that your drive is correctly mounted, and you have access to the required .whl file at the appropriate location. It can be anywhere, as long as you know where to find it. When you run the above, you must at least see oceanrl-0.1.0-py3-non-any.whl.

Get James Koh, PhD’s stories in your inbox

Join Medium for free to get updates from this writer.

Next, pip install it. You should see the ‘successfully installed’ message like below.

!pip install drive/MyDrive/wheels/oceanrl-0.1.0-py3-none-any.whl
Press enter or click to view image in full size
Screenshot from Colab after successful installation.

Disclaimer: Do not install from dubious sources. Otherwise, it is like seeing a random bottle of water in public and drinking it without knowing its contents — you probably don’t want to take such a risk.

Ready for use!

Finally, we are really to use the function. Let’s try it out to verify!

from oceanrl import query

# The numbers for b (salmon) and c (sharks) should fluctuate slightly due to
# inherent randomness in reproduction, to provide overfitting or cheating

for _ in range(20):
a, b, c = query(20000, 500, 8, 1)
print(a, b, c)

# 3625 20724 506
# 3625 20121 505
# ... ... ...
# 3625 19109 504
# 3625 19436 504

Notice here that we have done from oceanrl import query.

It is as though you have a main.py script and oceanrl.py script (which contains the query function) in the same folder. From main.py, you can import classes and functions from other .py files. In fact, they could even be in different folders. But over here, what we did was pip install the whl file, without the source code.

Take a step back to recall that we originally have the following structure, with [project].name="oceanrl" defined in pyproject.toml. The name in .toml is the distribution name, and hence the ‘oceanrl’ within oceanrl-0.1.0-py3-non-any.whl, but the reason we do from oceanrl import query is because of the folder oceanrl/ which defines the package name.

project/
├── pyproject.toml
└── oceanrl/
├── __init__.py
├── _internal_math.py
└── api.py

Difference from Docker

You may be saying that this looks like Docker. There are many similarities indeed! Both are bundles for third-party distribution. However, a Docker image encompass the OS base image, libraries , and language runtimes (Python, Java, etc.) — it bundles the entire runtime environment. On the other hand, a wheel is lightweight — it bundles only the Python package and its metadata (and hence is smaller in memory size).

Conclusion

In this short article, I showed the full process of creating your own Python wheel. With this, you are now able to let third-parties execute your functions, without explicitly revealing the raw source code.

Even if you don’t have such a need, you now have a better appreciation whenever you perform pip install!

Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.

--

James Koh, PhD
James Koh, PhD

Written by James Koh, PhD

Data Science Instructor - teaching Masters students

MITB For All
MITB For All

Published in MITB For All

Tech contents related to AI, Analytics, Fintech, and Digital Transformation. Written by MITB Alumni; open-access for everyone.