Alt text

Kotlin and Python are both popular programming languages that offer a range of unique features. In this article, we’ll compare these two languages in various scenarios, and provide code examples to help you decide which language is best suited for your needs. Drawing from my personal experience, I used Kotlin for more than two years during my tenure at Wonder and fell in love with its robustness and efficiency.

Introduction to Kotlin

Kotlin is a statically typed programming language developed by JetBrains. It is designed to be fully interoperable with Java, making it an attractive option for Java developers who want to leverage their existing skills and codebase. Kotlin is also the official language for Android app development. While Kotlin is known for Android development, it also excels in backend development. Its powerful features and enjoyable coding experience make it popular for server-side applications.

Pros of Kotlin

  • Interoperable with Java
  • Built-in null safety
  • Statically typed (big fan of this)
  • Modern syntax and features
  • Official language for Android development
  • Strong community and support

Introduction to Python

Python is a dynamically typed, high-level programming language known for its simplicity and readability. It has a wide range of applications, including web development, scientific computing, data analysis, artificial intelligence, and more.

Pros of Python

  • Easy-to-read syntax
  • Vast standard library
  • Strong community and support
  • Extensive third-party libraries
  • Versatile, suitable for various domains

Scoped Functions and Error Handling

Scoped functions are an elegant way to perform operations within a specific context or scope. They are useful for managing resources, such as files or network connections, and can simplify error handling.

Kotlin Example with Scoped Functions

To define the tryCatch() extension function, we first create a function that takes a lambda function as its argument and returns a Unit. The body of the function consists of a try-catch block that wraps the invocation of the lambda function. If an exception is thrown during the execution of the lambda function, the catch block will handle it by printing the stack trace.

Here’s the code for the tryCatch() extension function in Kotlin:

// Define the extension function
fun (() -> Unit).tryCatch() {
    try {
        this()
    } catch (e: Exception) {
        // Maybe you want to send logs to Kibana or some other external Observability system
        e.printStackTrace()
    }
}

// Usage
val someFunctionThatMayThrow = {
    // Some function which may throw an exception
}

// Now you can use tryCatch with it
someFunctionThatMayThrow.tryCatch()

By using this extension function, you can easily handle exceptions within the scope of any lambda function, making your code more concise and readable.

Python Example with Context Managers

In Python, we can achieve similar functionality using context managers and the with statement. Although not exactly the same as Kotlin’s scoped functions, context managers provide a clean way to handle resources within a specific block. Here’s an example of creating a custom context manager for handling exceptions in Python:

from contextlib import contextmanager

# Define the context manager
@contextmanager
def try_catch():
    try:
        yield
    except Exception as e:
        print(e)

# Usage
def some_function_that_may_throw():
    # Some function which may throw an exception

# Now you can use try_catch with it
with try_catch():
    some_function_that_may_throw()

This approach allows us to manage exceptions in a clean and concise manner in both Kotlin and Python.

However, there are some differences between the two languages:

  • In Python, you need to import an extra library to do what is baked in the Kotlin library.
  • Simultaneously, and perhaps this is just my personal take, the Kotlin example feels more practical and aligned with the daily tasks of a programmer. It is easier to pass the call to the object directly rather than using Python’s with statement.

Comparing Scope Functions: Kotlin’s let vs. Python’s Explicit Variable Declaration

In Kotlin, scope functions like let allow you to perform operations on an object within a block of code, making it easier to handle nullable values and reducing the need for temporary variables. In contrast, Python requires you to explicitly declare and manage variables when performing similar operations, leading to more code.

Kotlin: Using let

Kotlin’s let function can be particularly useful when working with nullable values. It allows you to execute a block of code only if the object is not null, and automatically casts the object inside the block to its non-nullable version. Here’s an example:

val nullableString: String? = "Hello, world!"
nullableString?.let { nonNullableString ->
    println("The string length is ${nonNullableString.length}")
}

In this example, we use the safe call operator (?.) combined with let. If nullableString is not null, then the block of code inside let will be executed and the string will be automatically cast as a non-nullable type.

Python: Explicit Variable Declaration

In Python, handling nullable values requires checking for their existence explicitly using conditional statements or ternary operators. Here’s an example:

nullable_string = "Hello, world!"
if nullable_string is not None:
    print(f"The string length is {len(non_nullable_string)}")

In this Python example, we first check if nullable_string is not None before assigning its value to non_nullable_string. We then proceed with our operation within the conditional block.

As demonstrated above, Kotlin provides a more concise way of handling nullability using scope functions like let, while Python requires developers to manage variables and null checks more explicitly.

Kotlin Enums and Smart Cast: Improved Maintainability

Kotlin’s enum classes and smart cast feature of the when expression provide a safer and more maintainable approach to handling multiple conditions. When using enums with when, Kotlin enforces exhaustive checks for all possible enum cases, ensuring that you handle every scenario at compile time.

Kotlin: Enum Classes and Exhaustive when

Kotlin’s enum classes enable you to define a type-safe enumeration of values. Combined with the powerful when expression, which supports smart casts, you can create robust code that handles all possible cases. Here’s an example:

enum class Color {
    RED, GREEN, BLUE
}

fun describeColor(color: Color): String {
    return when (color) {
        Color.RED -> "The color is red."
        Color.GREEN -> "The color is green."
        Color.BLUE -> "The color is blue."
    }
}

In this example, if you were to add another value to the Color enum without updating the describeColor function accordingly, the compiler would throw an error. This enforcement ensures that your code remains maintainable and less prone to bugs as changes occur in the future.

Python: Handling Enums in Conditional Statements

In Python, while you can use the built-in Enum class for creating enumerations, there’s no equivalent feature like Kotlin’s exhaustive when. Instead, you’ll need to use conditional statements such as if-elif-else. Here’s an example:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

def describe_color(color: Color) -> str:
    if color == Color.RED:
        return "The color is red."
    elif color == Color.GREEN:
        return "The color is green."
    elif color == Color.BLUE:
        return "The color is blue."
    else:
        raise ValueError("Invalid color")

In this Python example, new enum cases added without updating the describe_color function would not result in a compile-time error. Instead, you might encounter runtime errors due to unhandled cases.

The Problem with Python

In Python, when working with large enterprise-level codebases, the use of enums requires careful management. If an enum is extensively used throughout the application, developers must manually check all usages to ensure that each instance where the enum is evaluated is updated when new enum types are added. This can be a significant maintenance burden, as there is no built-in mechanism to enforce the handling of all enum cases, potentially leading to missed cases and runtime errors.

For example, consider an application with an OrderStatus enum used in multiple places to determine the workflow of an order. If a new status is introduced, developers must locate and update every conditional statement that evaluates OrderStatus to handle the new case. This manual process is error-prone and can result in bugs if any instance is overlooked.

from enum import Enum
class OrderStatus(Enum):
    PENDING = 1
    PROCESSING = 2
    SHIPPED = 3
    DELIVERED = 4
    # Imagine a new status is added here

# Example usage in the code
if order.status == OrderStatus.PENDING:
    # Handle pending status
elif order.status == OrderStatus.PROCESSING:
    # Handle processing status
# ... and so on for each status
# New enum cases must be manually added to each conditional block

This contrasts with Kotlin’s when expression, combined with enum classes, which enforces an exhaustive check, ensuring that all possible cases are handled at compile time, thus reducing the risk of errors and improving maintainability.

Overall, Kotlin’s enums and smart cast feature of the when expression provides a more reliable and maintainable approach compared to Python’s conditional statements. This makes Kotlin an attractive choice for projects where code maintainability and safety are critical considerations.

Conclusion

Kotlin and Python are powerful programming languages, each with their own strengths and suited for different domains and teams. In my experience, Python is fantastic for quick iteration, but it can become challenging and accrue tech debt as projects grow and require more team interactions. On the other hand, Kotlin shines in large, critical codebases, helping prevent many common mistakes that I often make in Python without relying on external tools like mypy or pyright.