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.
Gao
GaoFounder
August 28, 202312 min read
Learn Python in a weekend: From zero to 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:

  1. Create the Logto client for tasks like sign-in, sign-out, user info, and token management.
  2. Provide a tutorial and a sample project show the SDK usage.
  3. 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.

Don't fixate on minutiae at this stage. The goal is to get started as soon as possible. Opt for widely adopted tools and progress. Changes can always be made later.

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 to package.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:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
  return "<p>Hello, World!</p>"

Launching the server via flask --app hello run and navigating to http://localhost:5000 on my browser yielded the desired outcome. It worked!

The methodology for selecting Flask and additional packages mirrors the approach adopted for pyenv and Poetry. Given its repetitive nature, these specifics are omitted in subsequent sections.

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.

My go-to strategy is to first read the official documentation before searching for other resources. Usually it's the most efficient way to learn something new.

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:

class LogtoClient:
  def __init__(self, config = 'config'):
    self.config = config

  def signIn(self):
    return 'signIn' + self.config

Subsequently, integrate the class with Flask:

from logto import LogtoClient

client = LogtoClient()

@app.route("/")
def hello_world():
  return client.signIn()

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:

from typing import Optional

class LogtoClient:
  def __init__(self):
    self.config: str | None = 'config'

  def signIn(self) -> str:
    return 'signIn' + self.config

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:

class LogtoConfig:
  def __init__(self, clientId: str, clientSecret: str):
    self.clientId = clientId
    self.clientSecret = clientSecret
    # ...

It looks okay, but soon we'll need to face the problems of encoding, decoding, and validating the object from JSON.

Always prioritize built-in solutions. If they really don't work, then explore third-party libraries. Only when all alternatives falter, consider creating your own solution.

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:

from pydantic import BaseModel

class LogtoConfig(BaseModel):
  clientId: str
  clientSecret: str
  # ...

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.

I always remind myself about the goal. If a certain layer of knowledge is enough to achieve the goal, then I don't need to dig deeper since I can always come back later. Meanwhile, thorough comprehension is required within a specific layer. Thus I can handle various situations in the future. The key is to find the balance, and it's not easy. I may write another article about this topic in the future.

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:

@app.route("/")
async def hello_world():
  # ...

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:

def generateCodeChallenge(codeVerifier: str) -> str:
    """
    Generate a code challenge string for the given code verifier string.

    See: https://www.rfc-editor.org/rfc/rfc7636.html#section-4.2
    """

This posed a great prompt for Large Language Models (LLMs): comprising method definitions by clear comments. And Copilot didn't disappoint:

def generateCodeChallenge(codeVerifier: str) -> str:
    """
    Generate a code challenge string for the given code verifier string.

    See: https://www.rfc-editor.org/rfc/rfc7636.html#section-4.2
    """
    codeChallenge = hashlib.sha256(codeVerifier.encode('utf-8')).digest()
    return base64.urlsafe_b64encode(codeChallenge).decode('utf-8').replace('=', '')

Some tweaks might be required, but it doesn't matter. It already changed the game.

Copilot shines when producing code for straightforward logic. However, it may struggle when it involves multiple dependencies. As the name suggests, it's a code assistant, you should be the captain that controls the direction and makes the decision.

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.

Copilot is even better when writing tests, it can generate a multitude of practical test cases. Even when tweaks are necessary, Copilot can learn from the new context and enhance future code generation.

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!