
This article was originally published on the JetBrains blog.
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!"
language is of type String, and list is of type List<Int>. That allows not only low-level optimizations, but also safety and better IDE support. Those variables 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 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.fun test(list: MutableList<Int> = mutableListOf()) { list.add(1) println(list) } test() // prints [1] test() // prints [1] test() // prints [1]
def test(list=[]): list.append(1) print(list) test() # prints [1] test() # prints [1, 1] test() # prints [1, 1, 1]
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)
@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)
Optional type from the typing package.SqlitePostRepository, crud is expected to be passed in the constructor. The framework we use will take care of providing an instance of PostCrudRepository, that is generated automatically by Spring Data.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 )
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)
collections package. Those constructs are not very consistent, some of them are not very convenient, and 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.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" } }
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)
@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() }
fun main(): Unit = embeddedServer(Netty, port = 8080) { routing { var value = "" get("/text") { call.respondText(value) } post("/text") { value = call.receiveText() call.respond(HttpStatusCode.OK) } } }.start(wait = true)
asyncio, while in Kotlin has Kotlin Coroutines library, made by Kotlin creators. Both libraries can start lightweight asynchronous tasks and await their completion. However, there are some important differences between them.
asyncio introduced TaskGroup in version 3.11, which offers some support for structured concurrency, but it is far from what Kotlin Coroutines offer, and requires explicit usage.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()) }
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. In Kotlin Coroutines, coroutines can typically run on a thread pool (by default as big as the number of CPU cores). This way Kotlin Coroutines beter utilize the power of modern hardware. Of course, Kotlin Coroutines can also run on a single thread if needed, which is quite common in client applications.@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) }
Flow. 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.fun notificationStatusFlow(): Flow<NotificationStatus> = notificationProvider.observeNotificationUpdate() .distinctUntilChanged() .scan(NotificationStatus()) { status, update -> status.applyNotification(update) } .combine( userStateProvider.userStateFlow() ) { status, user -> statusFactory.produce(status, user) }

Benchmark comparing different languages performing the same nested loop iterations made by Ben Dicken and published on https://benjdd.com/languages/.

Benchmark comparing different languages performing the same Naive Fibonacci calculation made by Ben Dicken and published on https://benjdd.com/languages/.