Kotlin
A concise multiplatform language developed by JetBrains
From Python to Kotlin: A Transition Worth Making
This article was written by an external contributor.
When it comes to writing short scripts or CRUDs, Python is a great choice. With its rich ecosystem and broad adoption, it can be easily used to scrape some data or to perform data analysis. However, maintaining a large codebase in Python can be very problematic.
Python’s dynamic typing and mutable nature, while offering flexibility for rapid development, may present additional considerations in larger codebases. Event-loop-based coroutines can be tricky and may lead to subtle issues in practice . Finally, the single-threaded and dynamically typed nature of this language makes Python code significantly less efficient than most of its modern competitors.
JVM is one of the fastest runtime platforms, making Java nearly as efficient as C. Most benchmarks show that Python code is 10 to 100 times slower than Java code. One big research paper compared the performance of multiple languages and showed that Python code is 74x more CPU-expensive than code in C, where Java code is only 1.8x more expensive. However, due to its long-standing commitment to backward compatibility, Java can feel verbose for certain tasks. Kotlin, building on that same ecosystem and offering the same efficiency, gives you access to a powerful typesystem, with modern language features focused on performance and developer ergonomics.
Those are the key reasons we can hear from companies or teams that decide to switch from Python to Kotlin. The Kotlin YouTube channel recently published Wolt’s success story, but that is only one voice among many. Kotlin is an all-around sensible choice for a range of projects, as it shares many similarities with Python. At the same time, Kotlin offers better performance, safety, and a much more powerful concurrency model. Let’s see those similarities and differences in practice.
Similarities between Python and Kotlin
When teaching Kotlin to both Python and Java developers, I was often surprised to discover that many Kotlin features are more frequently reported as intuitive by Python developers than their Java counterparts. Both languages offer concise syntax. Let’s compare some very simple use cases in both languages:
val language = "Kotlin" println("Hello from $language") // prints "Hello from Kotlin" val list = listOf(1, 2, 3, 4, 5) for (item in list) { println(item) } // prints 1 2 3 4 5 each in a new line fun greet(name: String = "Guest") { println("Hello, $name!") } greet() // prints "Hello, Guest!"
language = "Python" print(f"Hello from {language}") # prints "Hello from Python" list = [1, 2, 3, 4, 5] for item in list: print(item) # prints 1 2 3 4 5 each in a new line def greet(name="Guest"): print(f"Hello, {name}!") greet() # prints "Hello, Guest!"
At first glance, there are only minor syntactic differences. Kotlin presents features well-known to Python developers, like string interpolation, concise loops, and default parameters. However, even in this simple example, we can see some advantages that Kotlin has over Python. All properties are statistically typed, so language
is of type String
, and list
is of type List<Int>
. That not only allows for low-level optimizations, but it also brings enhanced safety and better IDE support. All variables in the code above are also defined as immutable, so we cannot accidentally change their values. To change them, we would need to use var
instead of val
. The same goes for the list I used in this snippet – it is immutable, so we cannot accidentally change its content. To create a mutable list, we would need to use mutableListOf
and type it as MutableList<Int>
. This strong distinction between mutable and immutable types is a great way to avoid accidental changes, which are often the source of bugs in Python programs.
There are other advantages of Kotlin over Python that are similarly apparent in the above example. Python’s default arguments are static, so changing them influences all future calls. This is a well-known source of very sneaky bugs in Python programs. Kotlin’s default arguments are evaluated at each call, so they are safer.
Kotlin
fun test(list: MutableList<Int> = mutableListOf()) { list.add(1) println(list) } test() // prints [1] test() // prints [1] test() // prints [1]
Python
def test(list=[]): list.append(1) print(list) test() # prints [1] test() # prints [1, 1] test() # prints [1, 1, 1]
Let’s talk about classes. Both languages support classes, inheritance, and interfaces. To compare them, let’s look at a simple data class in both languages:
Kotlin
data class Post( val id: Int, val content: String, val publicationDate: LocalDate, val author: String? = null ) val post = Post(1, "Hello, Kotlin!", LocalDate.of(2024, 6, 1)) println(post) // prints Post(id=1, content=Hello, Kotlin!, publicationDate=2024-06-01, author=null)
Python
@dataclass class Post: id: int content: str publication_date: date author: Optional[str] = None post = Post(1, "Hello, Python!", datetime.date(2024, 6, 1)) print(post) # prints Post(id=1, content='Hello, Python!', publication_date=datetime.date(2024, 6, 1), author=null)
Kotlin has built-in support for data classes, which automatically allows such objects to be compared by value, destructured, and copied. Python requires an additional decorator to achieve similar functionality. This class is truly immutable in Kotlin, and thanks to static typing, it requires minimal memory. Outside of that, both implementations are very similar. Kotlin has built-in support for nullability, which in Python is expressed with the Optional
type from the typing
package.
Now, let’s define a repository interface and its implementation in both languages. In Kotlin, we can use Spring Data with coroutine support, while in Python, we can use SQLAlchemy with async support. Notice that in Kotlin, there are two kinds of properties: Those defined inside a bracket are constructor parameters, while those defined within braces are class properties. So in SqlitePostRepository
, crud
is expected to be passed in the constructor. The framework we use will provide an instance of PostCrudRepository
, which is generated automatically by Spring Data.
Kotlin
interface PostRepository { suspend fun getPost(id: Int): Post? suspend fun getPosts(): List<Post> suspend fun savePost(content: String, author: String): Post } @Service class SqlitePostRepository( private val crud: PostCrudRepository ) : PostRepository { override suspend fun getPost(id: Int): Post? = crud.findById(id) override suspend fun getPosts(): List<Post> = crud.findAll().toList() override suspend fun savePost(content: String, author: String): Post = crud.save(Post(content = content, author = author)) } @Repository interface PostCrudRepository : CoroutineCrudRepository<Post, Int> @Entity data class Post( @Id @GeneratedValue val id: Int? = null, val content: String, val publicationDate: LocalDate = LocalDate.now(), val author: String )
Python
class PostRepository(ABC): @abstractmethod async def get_post(self, post_id: int) -> Optional[Post]: pass @abstractmethod async def get_posts(self) -> List[Post]: pass @abstractmethod async def save_post(self, content: str, author: str) -> Post: pass class SqlitePostRepository(PostRepository): def __init__(self, session: AsyncSession): self.session = session async def get_post(self, post_id: int) -> Optional[Post]: return await self.session.get(Post, post_id) async def get_posts(self) -> List[Post]: result = await self.session.execute(select(Post)) return result.scalars().all() async def save_post(self, content: str, author: str) -> Post: post = Post(content=content, author=author) self.session.add(post) await self.session.commit() await self.session.refresh(post) return post class Post(Base): __tablename__ = "posts" id: Mapped[int] = Column(Integer, primary_key=True, index=True) content: Mapped[str] = Column(String) publication_date: Mapped[date] = Column(Date, default=date.today) author: Mapped[str] = Column(String)
Those implementations are very similar in many ways, and the key differences between them result from choices made by the frameworks, not the languages themselves. Python, due to its dynamic nature, encourages the use of untyped objects or dictionaries; however, such practices are generally discouraged in modern times. Both languages provide numerous tools for libraries to design effective APIs. On the JVM, these languages often depend on annotation processing, whereas in Python, decorators are more common. Kotlin leverages a mature and well-developed Spring Boot ecosystem, but it also offers lightweight alternatives such as Ktor or Micronaut. Python has Flask and FastAPI as popular lightweight frameworks, and Django as a more heavyweight framework.
In a backend application, we also need to implement services, which are classes that implement business logic. They often do some collection or string processing. Kotlin provides a comprehensive standard library with numerous useful functions for processing collections and strings. All those functions are named and called in a very consistent way. In Python, we can make nearly all transformations available in Kotlin, but to do so, we need to use many different kinds of constructs. In the code below, I needed to use top-level functions, methods on lists, collection comprehensions, or even classes from the collections
package. Those constructs are not very consistent, some of them are not very convenient, and are not easily discoverable. You can also see that complicated notation for defining lambda expressions in Python harms collection processing APIs. Collection and string processing in Kotlin is much more pleasant and productive.
Kotlin
class PostService( private val repository: PostRepository ) { suspend fun getPostsByAuthor(author: String): List<Post> = repository.getPosts() .filter { it.author == author } .sortedByDescending { it.publicationDate } suspend fun getAuthorsWithPostCount(): Map<String?, Int> = repository.getPosts() .groupingBy { it.author } .eachCount() suspend fun getAuthorsReport(): String = getAuthorsWithPostCount() .toList() .sortedByDescending { (_, count) -> count } .joinToString(separator = "n") { (author, count) -> val author = author ?: "Unknown" "$author: $count posts" } .let { "Authors Report:n$it" } }
Python
class PostService: def __init__(self, repository: "PostRepository") -> None: self.repository = repository async def get_posts_by_author(self, author: str) -> List[Post]: posts = await self.repository.get_posts() filtered = [post for post in posts if post.author == author] sorted_posts = sorted( filtered, key=lambda p: p.publication_date, reverse=True ) return sorted_posts async def get_authors_with_post_count(self) -> Dict[Optional[str], int]: posts = await self.repository.get_posts() counts = Counter(p.author for p in posts) return dict(counts) async def get_authors_report(self) -> str: counts = await self.get_authors_with_post_count() items = sorted(counts.items(), key=lambda kv: kv[1], reverse=True) lines = [ f"{(author if author is not None else 'Unknown')}: {count} posts" for author, count in items ] return "Authors Report:n" + "n".join(lines)
Before we finish our comparison, let’s complete our example backend application by defining a controller that exposes our service through HTTP. Until now, I have used Spring Boot, which is the most popular framework for Kotlin backend development. This is how it can be used to define a controller:
Kotlin
@Controller @RequestMapping("/posts") class PostController( private val service: PostService ) { @GetMapping("/{id}") suspend fun getPost(@PathVariable id: Int): ResponseEntity<Post> { val post = service.getPost(id) return if (post != null) { ResponseEntity.ok(post) } else { ResponseEntity.notFound().build() } } @GetMapping suspend fun getPostsByAuthor(@RequestParam author: String): List<Post> = service.getPostsByAuthor(author) @GetMapping("/authors/report") suspend fun getAuthorsReport(): String = service.getAuthorsReport() }
However, we noticed that many Python developers prefer a lighter and simpler framework, and their preferred choice for such functionality is Ktor. Ktor allows users to define a working application in just a couple of lines of code. This is a complete Ktor Server application that implements a simple in-memory text storage (it requires no other configuration or dependencies except Ktor itself):
Kotlin
fun main() = embeddedServer(Netty, port = 8080) { routing { var value = "" get("/text") { call.respondText(value) } post("/text") { value = call.receiveText() call.respond(HttpStatusCode.OK) } } }.start(wait = true)
I hope that this comparison helped you see both the key similarities and differences between Python and Kotlin. As we’ve seen, Kotlin has many features that are very intuitive for Python developers. At the same time, Kotlin offers many improvements over Python, especially in terms of safety. It has a powerful static type system that prevents many common bugs, built-in support for immutability, and a very rich and consistent standard library.
To summarize, I believe it’s fair to say that both languages are very similar in many ways, but Kotlin brings a number of improvements – some small, some big. In addition, Kotlin offers some unique features that are not present in Python, the biggest one probably being a concurrency model based on coroutines.
kotlinx.coroutines vs. Python asyncio
The most modern approach to concurrency in Kotlin and Python is based on coroutines. In Python, the most popular library for this purpose is asyncio
, while in Kotlin, there is the Kotlin kotlinx.coroutines
library. Both libraries can start lightweight asynchronous tasks and await their completion. However, there are some important differences between them.
Let’s start with the hallmark feature of kotlinx.coroutines
: first-class support for structured concurrency. Let’s say that you implement a service like SkyScanner, which searches for the best flight offers. Now, let’s suppose a user makes a search, which results in a request or the opening of a WebSocket connection to our service. Our service needs to query multiple airlines to return the best offers. Let’s then suppose that this user left our page soon after searching. All those requests to airlines are now useless and likely very costly, because we have a limited number of ports available to make requests. However, implementing explicit cancellation of all those requests is very hard. Structured concurrency solves that problem. With kotlinx.coroutines, every coroutine started by a coroutine is its child, and when the parent coroutine is cancelled, all its children are cancelled too. This way, our cancellation is automatic and reliable.

However, structured concurrency goes even further. If getting a resource requires loading two other resources asynchronously, an exception in one of those two resources will cancel the other one too. This way, kotlinx.coroutines ensures that we use our resources in the most efficient way. In Python, asyncio
introduced TaskGroup
in version 3.11, which offers some support for structured concurrency, but it is far from what kotlinx.coroutines offer, and it requires explicit usage.
Kotlin
suspend fun fetchUser(): UserData = coroutineScope { // fetchUserDetails is cancelled if fetchPosts fails val userDetails = async { api.fetchUserDetails() } // fetchPosts is cancelled if fetchUserDetails fails val posts = async { api.fetchPosts() } UserData(userDetails.await(), posts.await()) }
The second important difference is thread management. In Python, asyncio
runs all tasks on a single thread. Notice that this is not utilizing the power of multiple CPU cores, and it is not suitable for CPU-intensive tasks. Using kotlinx.coroutines, coroutines can typically run on a thread pool (by default as big as the number of CPU cores). This way, coroutines better utilize the power of modern hardware. Of course, coroutines can also run on a single thread if needed, which is quite common in client applications.
Another big advantage of coroutines is their testing capabilities. kotlinx.coroutines provides built-in support for testing asynchronous code over a predetermined simulated timeframe, removing the need to wait while the code is tested in real time. This way, we can test asynchronous code in a deterministic way, without any flakiness. We can also easily simulate all kinds of scenarios, like different delays from dependent services. In Python, testing asynchronous code is possible using third-party libraries, but this method is not as powerful and convenient as with coroutines.
Kotlin
@Test fun `should fetch data asynchronously`() = runTest { val api = mockk<Api> { coEvery { fetchUserDetails() } coAnswers { delay(1000) UserDetails("John Doe") } coEvery { fetchPosts() } coAnswers { delay(1000) listOf(Post("Hello, world!")) } } val useCase = FetchUserDataUseCase(api) val userData = useCase.fetchUser() assertEquals("John Doe", userData.user.name) assertEquals("Hello, world!", userData.posts.single().title) assertEquals(1000, currentTime) }
Finally, kotlinx.coroutines offer powerful support for reactive streams through the Flow
type. It is perfect for representing websockets or streams of events. Flow
processing can be easily transformed using operators consistent with collection processing. It also supports backpressure, which is essential for building robust systems. Python has async generators
, which can be used to represent streams of data, but they are not as powerful and convenient as Flow
.
Kotlin
fun notificationStatusFlow(): Flow<NotificationStatus> = notificationProvider.observeNotificationUpdate() .distinctUntilChanged() .scan(NotificationStatus()) { status, update -> status.applyNotification(update) } .combine( userStateProvider.userStateFlow() ) { status, user -> statusFactory.produce(status, user) }
Performance comparison
One of the key benefits of switching from Python to Kotlin is performance. Python applications can be fast when they use optimized native libraries, but Python itself is not the fastest language. As a statically typed language, Kotlin can be compiled to optimized bytecode that runs on the JVM platform, which is a highly optimized runtime. In consequence, Kotlin applications are typically faster than Python applications.


Kotlin applications also use fewer resources. One reason for this is Kotlin’s more efficient memory management (a consequence of static typing). Another reason is structured concurrency, which ensures that resources are cancelled when they are no longer needed.
Interoperability
Kotlin is fully interoperable with Java. This means that Kotlin applications can use everything from the rich Java ecosystem. Everything that can be used in Java can easily be used in Kotlin as well (see interoperability guide).
It is also possible to bridge between Kotlin and Python using libraries like JPype or Py4J. Nowadays, some libraries support further interoperability, like zodable, which allows generating Zod schemas from Kotlin data classes.
Summary
I love Kotlin and I love Python. I’ve used both languages extensively throughout my career. In the past, Python had many clear advantages over Kotlin, such as a richer ecosystem, more libraries, and scripting capabilities. In some domains, like artificial intelligence, I still find Python to be a better choice. However, for backend development, Kotlin is clearly the better option today. It offers similar conciseness and ease of use as Python, but it is faster, safer, and scales better. If you consider switching from Python to Kotlin for your backend development, it is a transition worth making.