Learn Python in a weekend: From zero to a complete project
How can we quickly learn a new programming language? In this article, we'll share our weekend experience of learning Python by building a complete project.
Introduction
Logto, as an identity service, providing a smooth experience across various programming languages and frameworks is essential. This often involves creating Software Development Kits (SDKs). However, when the programming language lies outside our tech stack and our team lacks familiarity, crafting a robust SDK becomes a challenge.
Python posed such a challenge for us. Despite having many Python users, we lacked a Python SDK, a lingering concern. Determined to address this, I started to bridge this gap.
Although I have years of programming experience, Python remains relatively uncharted territory for me. While I'd tinkered with Python 2 briefly for simple scripts years ago, my knowledge was outdated. Nevertheless, it was time to dive in!
Day 1: Lay the foundation
Define the goal
In my experience, the most effective approach to learning a new programming language is to build a complete project. Fortunately, our goal was clear: construct a Logto Python SDK for web applications.
Rather than jumping into code, let's break it down. Here's the list:
- Create the Logto client for tasks like sign-in, sign-out, user info, and token management.
- Provide a tutorial and a sample project show the SDK usage.
- Publish the SDK to somewhere so that users can install it easily.
Looks like task 1 has the most work, so we need to confirm the scope and continue to break it down. This step is crucial to secure the boundary of the project, and avoid scope creep and over-engineering.
Previous work by our team saved me tons of time:
- A SDK convention outlined the structure and API design of SDKs.
- Existing SDKs for different languages provided insights into patterns and potential enhancements for Python.
Referencing these resources, I can see a clear picture of what to do. While specifics are beyond this article's scope, let's move forward.
Setup the environment
I'm using a Mac, so Python is already installed. However, I was wondering if there's a better way to manage the Python versions (I've heard the pain of version compatibility), just like nvm
for Node.js. Quickly, I found pyenv and directly started to install it.
Next on the agenda: a package and dependency manager. Usually they are coupled. So why not pip
(the Python default)? If you take a look at the requirements.txt
, you'll find that it's just a list of packages with versions. This is not enough for an SDK which may be used by other projects. For example, we may need to add some packages for development, but we don't want to include them in the final SDK. The requirements.txt
is too simple to handle this.
One of the benefits when you have other programming languages in your tech stack is that you can search for the "Python equivalent". So I searched for "package.json Python equivalent" and found Poetry, a stellar candidate:
- It functions as a package manager, dependency manager, and virtual environment manager.
- It has a
pyproject.toml
file, akin topackage.json
in Node.js. - It employs a lock file to log precise dependency versions.
Modern CLIs often encompass an init
command tailored for new projects. So does Poetry. I ran the command and it created a pyproject.toml
file for me.
The first line of code
At last, the moment to write code had arrived. Initiating with the classic "Hello, World!" program is always a good choice. When learning a programming language, full-featured IDEs aren't always essential; an editor backed by a strong community, such as VS Code, is entirely adequate.
Given our SDK's focus on web applications, I started with a simple web server utilizing the popular framework Flask.
Leveraging Poetry's capabilities, installing Flask can be easily done by running poetry add flask
. Then, following Flask's official quickstart guide, I composed a 'hello.py' file with the following snippet:
Launching the server via flask --app hello run
and navigating to http://localhost:5000 on my browser yielded the desired outcome. It worked!
As a beginner, I was not hurry for writing more code. Instead, there are plenty of information to receive from the code snippet:
- Use
from x import y
to import a module or a class. - No semicolon to terminate lines (oh no).
- We can define a new variable by entering arbitrary name and assign a value to it.
- Creating an instance of a class sans the
new
keyword. - Python supports decorators, and the
@app.route
serves as a decorator that registers a function as a route handler.- The function's return value is interpreted as the response body.
- We can define a function by using
def
keyword.
As you can see, if we try to understand every line of code instead of "just make it work", we can learn a lot from it. Meanwhile, Flask's official documentation further explained the snippet in details.
Project kickoff
Now it's time to start the project. Soon I defined a LogtoClient
class and tried to add some properties and methods to feel the language:
Subsequently, integrate the class with Flask:
It started to feel like a real project. But I felt something was missing: a type system.
Type system
Since it's a SDK, incorporating a type system will help users to understand the API and reduce the chance of errors when developing.
Python introduced type hints in version 3.5. It's not as powerful as TypeScript, but it's better than nothing. I added some type hints to the LogtoClient
class:
Looks much better now. But the challenge raised when it comes to a complex type like an object with predefined keys. For example, we need to define a LogtoConfig
class to represent the config object:
It looks okay, but soon we'll need to face the problems of encoding, decoding, and validating the object from JSON.
After some research, I chose pydantic as the solution. It's a data validation library which works with type hints. It supports diverse JSON functionalities without following tedious boilerplate code.
Thus, the LogtoConfig
class can be rewritten as:
It also taught me about class inheritance in Python by appending parentheses after the class name.
Asynchronous operations
Within the Logto SDK, we need to make HTTP requests to the Logto server. If you have experience with JavaScript, the phrase "callback hell" likely rings a bell. It's a common problem when dealing with asynchronous operations. Modern programming languages present similar solutions like Promise
or coroutine
.
Luckily, Python has a built-in solution of async
and await
. Before using them, ensure the compatibility with popular frameworks. In Flask, this can be done by installing the async
extra and using async def
instead of def
:
Then we can use await
to wait for the result of an asynchronous operation.
HTTP requests
HTTP requests is an interesting topic. Almost every programming language has a native solution, but developers usually use a third-party library if for ease of use. Some examples:
- JavaScript:
XMLHttpRequest
vs.fetch
vs.axios
- Swift:
URLSession
vs.Alamofire
- Java:
HttpURLConnection
vs.OkHttp
This is also true for Python. My decision was to use aiohttp as it supports async
and await
, coupled with its popularity.
The Copilot magic
Before Copilot, we should now come to the tedious part of writing business logic. With the help of the SDK convention and other SDKs, I can write the descriptive comments for each method before writing the code.
It adds more code readability, also helps developers to understand the API right in the IDE or editor via code intelligence.
For instance, consider the generateCodeChallenge
method, the comments can be written as:
This posed a great prompt for Large Language Models (LLMs): comprising method definitions by clear comments. And Copilot didn't disappoint:
Some tweaks might be required, but it doesn't matter. It already changed the game.
Wrap-up
That's pretty much the progress achieved on the first day. It was a long day, but with modern tools and technologies, it was way better than I expected.
Day 2: Raise the bar
Based on the work of the first day, business logic was done rapidly. But for an SDK, it's still insufficient. Here are the tasks for the second day:
- Add unit tests.
- Enforce code formatting.
- Verify Python version compatibility.
- Add continuous integration.
- Publish the SDK.
Unit tests
Unit tests saved us many times, so I won't skip it. Here are common considerations when writing unit tests:
- How to organize and run the tests?
- How to assert the result?
- How to run asynchronous tests? (It sounds like a no-brainer, but it occasionally triggers problems in some languages.)
- How to mock the dependencies? (Don't dive into this topic until it's indispensable, as it can lead to rabbit holes.)
- How to generate code coverage reports?
With these questions in mind, I found the built-in unittest
module fell short for some cases. So I chose pytest as the test framework. It supports asynchronous tests and looks mature enough.
The journey revealed some interesting new concepts like fixture
to me. This can also benefit the mindset when writing code in other languages.
Code formatting
Every language has its own code formatting styles. Personally, consistent formatting can make me happy and comfortable; it's also helpful for code review and collaboration.
Rather than browsing the argument of the "best" style, I decided to pick an opinionated formatter and stick to it.
Black looks like a good choice. The only frustration is the unmodifiable tab size. But it's not a big deal, I chose to accommodate it.
Python version compatibility
As an SDK, it should be compatible across prevalent Python versions. By searching "python version usage statistics", I determined to use Python 3.8 as the minimum version.
The virtue of environment manager now shows up. I can easily toggle the Python version by running pyenv local 3.8
and poetry env use 3.8
. I could then run the tests to unveil compatibility issues.
Continuous integration
Continuous integration guarantees the quality of every code change. As our repository was hosted on GitHub, GitHub Actions presented the natural choice.
The core workflow follows straightforward principles:
- Setup the environment.
- Install dependencies.
- Build the project (No need for Python though).
- Run the tests.
GitHub Actions has a good community, so it only takes a few minutes to construct the workflow.
By employing matrix strategies, we can run the workflow on different Python versions, even on different operating systems.
Publish the SDK
The final step is to publish the SDK. For public packages, this can be typically done by submitting to the official language-specific package registry. For example, npm for Node.js, PyPI for Python, and CocoaPods for Swift.
Poetry is my guiding star. Just run poetry publish
to publish the package to PyPI. It's that simple.
Closing thoughts
It was an engaging journey. Without the help of the open-source community, it would be much harder. Applaud to all the contributors!
Here are some general takeaways:
- Precisely define the goal and break it down, then always keep the goal in mind.
- Setup a stable and reproducible development environment.
- Use (good) tools as much as possible.
- Prioritize built-in or existing solutions.
- Understand language conventions and every line of code you write.
- However, don't fixate on minutiae.
- Use Copilot for clear, descriptive tasks.
You can find the ultimate outcome in this repository. With the same strategy, I also quickly built the Logto PHP SDK. Don't hesitate to let us know if you have any suggestions.
Hope this article is helpful for learning a new programming language. Happy hacking!